/*
 *  VBI proxy client
 *
 *  Copyright (C) 2003-2004 Tom Zoerner
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 *  $Id: proxy-client.c,v 1.12 2006/05/31 03:54:28 mschimek Exp $
 */

static const char rcsid[] = "$Id: proxy-client.c,v 1.12 2006/05/31 03:54:28 mschimek Exp $";

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>

#include "vbi.h"
#include "io.h"
#include "bcd.h"
#include "misc.h"

#include "proxy-msg.h"
#include "proxy-client.h"

#ifdef ENABLE_PROXY

#define dprintf1(fmt, arg...)    do {if (vpc->trace >= 1) fprintf(stderr, "proxy-client: " fmt, ## arg);} while(0)
#define dprintf2(fmt, arg...)    do {if (vpc->trace >= 2) fprintf(stderr, "proxy-client: " fmt, ## arg);} while(0)

/* ----------------------------------------------------------------------------
** Declaration of types of internal state variables
*/
typedef enum
{
   CLNT_STATE_NULL,
   CLNT_STATE_ERROR,
   CLNT_STATE_WAIT_CON_CNF,
   CLNT_STATE_WAIT_IDLE,
   CLNT_STATE_WAIT_SRV_CNF,
   CLNT_STATE_WAIT_RPC_REPLY,
   CLNT_STATE_CAPTURING,
} PROXY_CLIENT_STATE;

struct vbi_proxy_client
{
   unsigned int            services;
   int                     strict;
   int                     buffer_count;
   int                     scanning;
   unsigned int            trace;
   VBI_PROXY_CLIENT_FLAGS  client_flags;
   VBI_PROXY_DAEMON_FLAGS  daemon_flags;
   VBI_DRIVER_API_REV      vbi_api_revision;
   vbi_raw_decoder         dec;

   int                     chn_scanning;
   int                     chn_prio;
   vbi_bool                has_token;
   int                     ctrl_fd;

   vbi_bool                sliced_ind;
   vbi_capture_buffer      raw_buf;
   vbi_capture_buffer      slice_buf;
   vbi_capture             capt_api;

   VBI_PROXY_EV_TYPE       ev_mask;

   PROXY_CLIENT_STATE      state;
   VBIPROXY_MSG_STATE      io;
   VBIPROXY_MSG          * p_client_msg;
   int                     max_client_msg_size;
   vbi_bool                endianSwap;
   unsigned long           rxTotal;
   unsigned long           rxStartTime;
   char                  * p_srv_host;
   char                  * p_srv_port;
   char                  * p_client_name;
   char                  * p_errorstr;

   VBI_PROXY_CLIENT_CALLBACK * p_callback_func;
   void                      * p_callback_data;
};

/* timeout for RPC to proxy daemon (for parameter changes) */
#define RPC_TIMEOUT_MSECS   5000
/* timeout for waiting until ongoing read is completed
** used to "free" the socket before sending parameter requests */
#define IDLE_TIMEOUT_MSECS  2000

/* helper macro */
#define VBI_RAW_SERVICES(SRV)           (((SRV) & (VBI_SLICED_VBI_625 | VBI_SLICED_VBI_525)) != 0)

/* ----------------------------------------------------------------------------
** Open client connection
** - automatically chooses the optimum transport: TCP/IP or pipe for local
** - since the socket is made non-blocking, the result of the connect is not
**   yet available when the function finishes; the caller has to wait for
**   completion with select() and then query the socket error status
*/
static vbi_bool proxy_client_connect_server( vbi_proxy_client * vpc )
{
   vbi_bool use_tcp_ip;
   int  sock_fd;
   vbi_bool result = FALSE;

   use_tcp_ip = FALSE;

   /* check if a server address has been configured */
   if ( ((vpc->p_srv_host != NULL) || (use_tcp_ip == FALSE)) &&
        (vpc->p_srv_port != NULL))
   {
      sock_fd = vbi_proxy_msg_connect_to_server(use_tcp_ip, vpc->p_srv_host, vpc->p_srv_port, &vpc->p_errorstr);
      if (sock_fd != -1)
      {
         /* initialize IO state */
         memset(&vpc->io, 0, sizeof(vpc->io));
         vpc->io.sock_fd    = sock_fd;
         vpc->io.lastIoTime = time(NULL);
         vpc->rxStartTime   = vpc->io.lastIoTime;
         vpc->rxTotal       = 0;

         result = TRUE;
      }
   }
   else
   {
      dprintf1("connect_server: hostname or port not configured\n");
      if (use_tcp_ip && (vpc->p_srv_host == NULL))
	 asprintf(&vpc->p_errorstr, _("Server hostname not configured."));
      else if (vpc->p_srv_port == NULL)
         asprintf(&vpc->p_errorstr, _("Server port not configured."));
   }
   return result;
}

/* ----------------------------------------------------------------------------
** Allocate buffer for client/servier message exchange
** - buffer is allocated statically, large enough for all expected messages
*/
static vbi_bool proxy_client_alloc_msg_buf( vbi_proxy_client * vpc )
{
   vbi_bool result;
   size_t msg_size;

   msg_size = sizeof(VBIPROXY_MSG_BODY);

   if ( (vpc->state == CLNT_STATE_CAPTURING) &&
        (vpc->services != 0) )
   {
      /* XXX TODO allow both raw and sliced */
      if (VBI_RAW_SERVICES(vpc->services))
         msg_size = VBIPROXY_SLICED_IND_SIZE(0, vpc->dec.count[0] + vpc->dec.count[1]);
      else
         msg_size = VBIPROXY_SLICED_IND_SIZE(vpc->dec.count[0] + vpc->dec.count[1], 0);

      if (msg_size < sizeof(VBIPROXY_MSG_BODY))
         msg_size = sizeof(VBIPROXY_MSG_BODY);
   }
   else
      msg_size = sizeof(VBIPROXY_MSG_BODY);

   msg_size += VBIPROXY_MSG_BODY_OFFSET;

   if (((int) msg_size != vpc->max_client_msg_size)
       || (vpc->p_client_msg == NULL))
   {
      if (vpc->p_client_msg != NULL)
         free(vpc->p_client_msg);

      dprintf2("alloc_msg_buf: allocate buffer for "
	       "max. %lu bytes\n", (unsigned long) msg_size);
      vpc->max_client_msg_size = msg_size;
      vpc->p_client_msg = malloc(msg_size);

      if (vpc->p_client_msg == NULL)
      {
         asprintf(&vpc->p_errorstr, _("Virtual memory exhausted."));
         result = FALSE;
      }
      else
         result = TRUE;
   }
   else
      result = TRUE;

   return result;
}

/* ----------------------------------------------------------------------------
** Checks the size of a message from server to client
*/
static vbi_bool proxy_client_check_msg( vbi_proxy_client * vpc, uint len,
                                        VBIPROXY_MSG * pMsg )
{
   VBIPROXY_MSG_HEADER * pHead = &pMsg->head;
   VBIPROXY_MSG_BODY * pBody = &pMsg->body;
   vbi_bool result = FALSE;

   /*if (vpc->p_client_msg->head.type != MSG_TYPE_SLICED_IND) */
   dprintf2("check_msg: recv msg type %d, len %d (%s)\n", pHead->type, pHead->len, vbi_proxy_msg_debug_get_type_str(pHead->type));

   switch (pHead->type)
   {
      case MSG_TYPE_CONNECT_CNF:
         if ( (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->connect_cnf)) &&
              (memcmp(pBody->connect_cnf.magics.protocol_magic, VBIPROXY_MAGIC_STR, VBIPROXY_MAGIC_LEN) == 0) )
         {
            if (pBody->connect_cnf.magics.endian_magic == VBIPROXY_ENDIAN_MAGIC)
            {  /* endian type matches -> no swapping required */
               vpc->endianSwap = FALSE;
            }
            else if (pBody->connect_cnf.magics.endian_magic == VBIPROXY_ENDIAN_MISMATCH)
            {  /* endian type does not match -> convert "endianess" of all msg elements > 1 byte */
               /* enable byte swapping for all following messages */
               vpc->endianSwap = TRUE;
            }
            result = TRUE;
         }
         break;

      case MSG_TYPE_CONNECT_REJ:
         result = ( (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->connect_rej)) &&
                    (memcmp(pBody->connect_rej.magics.protocol_magic, VBIPROXY_MAGIC_STR, VBIPROXY_MAGIC_LEN) == 0) );
         break;

      case MSG_TYPE_SLICED_IND:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) +
                          VBIPROXY_SLICED_IND_SIZE(pBody->sliced_ind.sliced_lines, pBody->sliced_ind.raw_lines));
         break;

      case MSG_TYPE_SERVICE_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->service_cnf));
         break;

      case MSG_TYPE_SERVICE_REJ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->service_rej));
         break;

      case MSG_TYPE_CLOSE_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER));
         break;

      case MSG_TYPE_CHN_TOKEN_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_token_cnf));
         break;

      case MSG_TYPE_CHN_TOKEN_IND:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_token_ind));
         break;

      case MSG_TYPE_CHN_NOTIFY_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_notify_cnf));
         break;

      case MSG_TYPE_CHN_SUSPEND_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_suspend_cnf));
         break;

      case MSG_TYPE_CHN_SUSPEND_REJ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_suspend_rej));
         break;

      case MSG_TYPE_CHN_IOCTL_CNF:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) +
                          VBIPROXY_CHN_IOCTL_CNF_SIZE(pBody->chn_ioctl_cnf.arg_size));
         break;

      case MSG_TYPE_CHN_IOCTL_REJ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_ioctl_rej));
         break;

      case MSG_TYPE_CHN_RECLAIM_REQ:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_reclaim_req));
         break;

      case MSG_TYPE_CHN_CHANGE_IND:
         result = (len == sizeof(VBIPROXY_MSG_HEADER) + sizeof(pBody->chn_change_ind));
         break;

      case MSG_TYPE_CONNECT_REQ:
      case MSG_TYPE_SERVICE_REQ:
      case MSG_TYPE_CHN_TOKEN_REQ:
      case MSG_TYPE_CHN_RECLAIM_CNF:
      case MSG_TYPE_CHN_NOTIFY_REQ:
      case MSG_TYPE_CHN_SUSPEND_REQ:
      case MSG_TYPE_CHN_IOCTL_REQ:
      case MSG_TYPE_DAEMON_PID_REQ:
      case MSG_TYPE_DAEMON_PID_CNF:
         dprintf1("check_msg: recv server msg type %d (%s)\n", pHead->type, vbi_proxy_msg_debug_get_type_str(pHead->type));
         result = FALSE;
         break;
      default:
         dprintf1("check_msg: unknown msg type %d\n", pHead->type);
         result = FALSE;
         break;
   }

   if (result == FALSE)
   {
      dprintf1("check_msg: illegal msg len %d for type %d (%s)\n", len, pHead->type, vbi_proxy_msg_debug_get_type_str(pHead->type));
      errno = EMSGSIZE;
   }

   return result;
}

/* ----------------------------------------------------------------------------
** Handle asynchronous messages from server
*/
static vbi_bool proxy_client_take_message( vbi_proxy_client * vpc )
{
   VBIPROXY_MSG_BODY * pMsg = &vpc->p_client_msg->body;
   vbi_bool result = FALSE;

   switch (vpc->p_client_msg->head.type)
   {
      case MSG_TYPE_SLICED_IND:
         if (vpc->state == CLNT_STATE_CAPTURING)
         {
            /* XXX TODO check raw */
            if ((int) pMsg->sliced_ind.sliced_lines > vpc->dec.count[0] + vpc->dec.count[1])
            {  /* more lines than req. for service -> would overflow the allocated slicer buffer
               ** -> discard extra lines (should never happen; proxy checks for line counts) */
               dprintf1("take_message: SLICED_IND: too many lines: %d > %d\n", pMsg->sliced_ind.sliced_lines, vpc->dec.count[0] + vpc->dec.count[1]);
               pMsg->sliced_ind.sliced_lines = vpc->dec.count[0] + vpc->dec.count[1];
            }
            /*assert(vpc->sliced_ind == FALSE);*/
            vpc->sliced_ind = TRUE;
            result = TRUE;
         }
         else if ( (vpc->state == CLNT_STATE_WAIT_IDLE) ||
                   (vpc->state == CLNT_STATE_WAIT_SRV_CNF) ||
                   (vpc->state == CLNT_STATE_WAIT_RPC_REPLY) )
         {
            /* discard incoming data during service changes */
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_TOKEN_IND:
         if ( (vpc->state == CLNT_STATE_CAPTURING) ||
              (vpc->state == CLNT_STATE_WAIT_IDLE) ||
              (vpc->state == CLNT_STATE_WAIT_RPC_REPLY) )
         {
            /* XXX check if we're currently waiting for CNF for chn param change? */
            vpc->has_token  = TRUE;
            vpc->ev_mask   |= VBI_PROXY_EV_CHN_GRANTED;
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_RECLAIM_REQ:
         if (vpc->state >= CLNT_STATE_WAIT_IDLE)
         {
            /* XXX FIXME: if no callback registered reply immediately */
            /* XXX FIXME? handle "has_token == FALSE": reply immediately? */
            vpc->ev_mask |= VBI_PROXY_EV_CHN_RECLAIMED;
            vpc->ev_mask &= ~VBI_PROXY_EV_CHN_GRANTED;
            result = TRUE;
         }
         break;

      case MSG_TYPE_CHN_CHANGE_IND:
         dprintf1("channel change indication: new scanning %d\n", pMsg->chn_change_ind.scanning);
         vpc->chn_scanning = pMsg->chn_change_ind.scanning;
         /* schedule callback to be invoked for this event */
         if ((pMsg->chn_change_ind.notify_flags & VBI_PROXY_CHN_FLUSH) != 0)
            vpc->ev_mask |= VBI_PROXY_EV_CHN_CHANGED;
         if ((pMsg->chn_change_ind.notify_flags & VBI_PROXY_CHN_NORM) != 0)
            vpc->ev_mask |= VBI_PROXY_EV_NORM_CHANGED;
         result = TRUE;
         break;

      case MSG_TYPE_CLOSE_REQ:
         result = FALSE;
         break;

      case MSG_TYPE_CONNECT_CNF:
      case MSG_TYPE_CONNECT_REJ:
      case MSG_TYPE_SERVICE_CNF:
      case MSG_TYPE_SERVICE_REJ:
      case MSG_TYPE_CHN_TOKEN_CNF:
      case MSG_TYPE_CHN_NOTIFY_CNF:
      case MSG_TYPE_CHN_SUSPEND_CNF:
      case MSG_TYPE_CHN_SUSPEND_REJ:
      case MSG_TYPE_CHN_IOCTL_CNF:
      case MSG_TYPE_CHN_IOCTL_REJ:
         /* synchronous message - internal error */
         dprintf1("take_message: error: handler called for RPC message reply %d (%s)\n", vpc->p_client_msg->head.type, vbi_proxy_msg_debug_get_type_str(vpc->p_client_msg->head.type));
         result = FALSE;
         break;

      default:
         break;
   }

   if ((result == FALSE) && (vpc->p_errorstr == NULL))
   {
      dprintf1("take_message: message type %d (len %d) not expected in state %d\n", vpc->p_client_msg->head.type, vpc->p_client_msg->head.len, vpc->state);
      asprintf(&vpc->p_errorstr, _("Protocol error (unexpected message)."));
   }

   return result;
}

/* ----------------------------------------------------------------------------
** Close client connection
*/
static void proxy_client_close( vbi_proxy_client * vpc )
{
   int save_errno;

   if (vpc != NULL)
   {
      save_errno = errno;
      vbi_proxy_msg_close_io(&vpc->io);

      memset(&vpc->io, 0, sizeof(vpc->io));
      vpc->io.sock_fd    = -1;
      vpc->io.lastIoTime = time(NULL);

      if (vpc->state != CLNT_STATE_NULL)
      {
         vpc->state = CLNT_STATE_ERROR;
      }
      errno = save_errno;
   }
   else
      dprintf1("proxy_client-close: illegal NULL ptr param");
}

/* ----------------------------------------------------------------------------
** Wait for I/O event on socket with the given timeout
*/
static int proxy_client_wait_select( vbi_proxy_client * vpc, struct timeval * timeout )
{
   struct timeval tv_start;
   struct timeval tv;
   fd_set fd_rd;
   fd_set fd_wr;
   int    ret;

   if (vpc->io.sock_fd != -1)
   {
      do
      {
#ifdef HAVE_LIBPTHREAD
         pthread_testcancel();
#endif

         FD_ZERO(&fd_rd);
         FD_ZERO(&fd_wr);

         if (vpc->io.writeLen > 0)
            FD_SET(vpc->io.sock_fd, &fd_wr);
         else
            FD_SET(vpc->io.sock_fd, &fd_rd);

         if ( ((vpc->client_flags & VBI_PROXY_CLIENT_NO_TIMEOUTS) == 0) &&
              ((vpc->daemon_flags & VBI_PROXY_DAEMON_NO_TIMEOUTS) == 0) )
         {
            tv = *timeout; /* Linux kernel overwrites this */
            gettimeofday(&tv_start, NULL);

            ret = select(vpc->io.sock_fd + 1, &fd_rd, &fd_wr, NULL, &tv);

            vbi_capture_io_update_timeout(timeout, &tv_start);
         }
         else
            ret = select(vpc->io.sock_fd + 1, &fd_rd, &fd_wr, NULL, NULL);

      } while ((ret < 0) && (errno == EINTR));

      if (ret > 0) {
	 dprintf2("wait_select: waited for %c -> sock r/w %d/%d\n",
		  (vpc->io.writeLen > 0) ? 'w':'r',
		  (int) FD_ISSET(vpc->io.sock_fd, &fd_rd),
		  (int) FD_ISSET(vpc->io.sock_fd, &fd_wr));
      } else if (ret == 0) {
	 dprintf1("wait_select: timeout\n");
      } else {
	 dprintf1("wait_select: error %d (%s)\n", errno, strerror(errno));
      }
   }
   else
   {
      dprintf1("wait_select: socket not open\n");
      ret = -1;
   }

   return ret;
}

/* ----------------------------------------------------------------------------
** Call remote procedure, i.e. write message then wait for reply
** - message must already have been written prior to calling this function
** - this is a synchronous message exchange with the daemon, i.e. the function
**   does not return until a reply is available or a timeout occured (in which
**   case the connection is dropped.)
*/
static vbi_bool proxy_client_rpc( vbi_proxy_client * vpc,
                                  VBIPROXY_MSG_TYPE reply1, VBIPROXY_MSG_TYPE reply2 )
{
   struct timeval tv;
   vbi_bool io_blocked;

   assert (vpc->state != CLNT_STATE_ERROR);
   assert (vpc->io.sock_fd != -1);

   tv.tv_sec  = RPC_TIMEOUT_MSECS / 1000;
   tv.tv_usec = (RPC_TIMEOUT_MSECS % 1000) * 1000;

   /* wait for write to finish */
   do
   {
      if (proxy_client_wait_select(vpc, &tv) <= 0)
         goto failure;

      if (vbi_proxy_msg_handle_write(&vpc->io, &io_blocked) == FALSE)
         goto failure;

   } while (vpc->io.writeLen > 0);

   /* wait for reply message */
   while (1)
   {
      assert (vbi_proxy_msg_is_idle(&vpc->io));

      do
      {
         if (proxy_client_wait_select(vpc, &tv) <= 0)
            goto failure;

         if (vbi_proxy_msg_handle_read(&vpc->io, &io_blocked, TRUE, vpc->p_client_msg, vpc->max_client_msg_size) == FALSE)
            goto failure;

      } while ((vpc->io.readOff == 0) || (vpc->io.readOff < vpc->io.readLen));

      /* perform security checks on received message */
      if (proxy_client_check_msg(vpc, vpc->io.readLen, vpc->p_client_msg) == FALSE)
         goto failure;

      vpc->rxTotal += vpc->p_client_msg->head.len;
      vbi_proxy_msg_close_read(&vpc->io);

      /* if it's the expected reply, we're finished */
      if ( (vpc->p_client_msg->head.type != reply1) &&
           (vpc->p_client_msg->head.type != reply2) )
      {
         /* process asynchronous message (e.g. slicer data or another IND message) */
         if (proxy_client_take_message(vpc) == FALSE)
            goto failure;
      }
      else
         break;
   }

   return TRUE;

failure:
   asprintf(&vpc->p_errorstr, _("Connection lost due to I/O error."));
   return FALSE;
}

/* ----------------------------------------------------------------------------
** Read a message from the socket
** - if no data is available in the socket buffer the function blocks;
**   when the timeout is reached the function returns 0
*/
static int proxy_client_read_message( vbi_proxy_client * vpc,
                                      struct timeval * p_timeout )
{
   vbi_bool io_blocked;
   int  ret;

   /* simultaneous read and write is not supported */
   assert (vpc->io.writeLen == 0);
   assert ((vpc->io.readOff == 0) || (vpc->io.readLen < vpc->io.readOff));

   if (proxy_client_alloc_msg_buf(vpc) == FALSE)
      goto failure;

   do
   {
      ret = proxy_client_wait_select(vpc, p_timeout);
      if (ret < 0)
         goto failure;
      if (ret == 0)
         break;

      if (vbi_proxy_msg_handle_read(&vpc->io, &io_blocked, TRUE, vpc->p_client_msg, vpc->max_client_msg_size) == FALSE)
         goto failure;

   } while (vpc->io.readOff < vpc->io.readLen);

   if (ret > 0)
   {
      /* perform security checks on received message */
      if (proxy_client_check_msg(vpc, vpc->io.readLen, vpc->p_client_msg) == FALSE)
         goto failure;

      vpc->rxTotal += vpc->p_client_msg->head.len;
      vbi_proxy_msg_close_read(&vpc->io);

      /* process the message - frees the buffer if neccessary */
      if (proxy_client_take_message(vpc) == FALSE)
         goto failure;
   }

   return ret;

failure:
   asprintf(&vpc->p_errorstr, _("Connection lost due to I/O error."));
   proxy_client_close(vpc);
   return -1;
}

/* ----------------------------------------------------------------------------
** Wait until ongoing read is finished
** - incoming data is discarded
*/
static vbi_bool proxy_client_wait_idle( vbi_proxy_client * vpc )
{
   PROXY_CLIENT_STATE old_state;
   struct timeval tv;
   vbi_bool io_blocked;

   assert (vpc->io.writeLen == 0);

   if (vpc->io.readOff > 0)
   {
      /* set intermediate state so that incoming data is discarded in the handler */
      tv.tv_sec  = IDLE_TIMEOUT_MSECS / 1000;
      tv.tv_usec = IDLE_TIMEOUT_MSECS * 1000;

      while (vpc->io.readOff < vpc->io.readLen)
      {
         if (proxy_client_wait_select(vpc, &tv) <= 0)
            goto failure;

         if (vbi_proxy_msg_handle_read(&vpc->io, &io_blocked, TRUE, vpc->p_client_msg, vpc->max_client_msg_size) == FALSE)
            goto failure;
      }

      /* perform security checks on received message */
      if (proxy_client_check_msg(vpc, vpc->io.readLen, vpc->p_client_msg) == FALSE)
         goto failure;

      vpc->rxTotal += vpc->p_client_msg->head.len;
      vbi_proxy_msg_close_read(&vpc->io);

      old_state = vpc->state;
      vpc->state = CLNT_STATE_WAIT_IDLE;

      if (proxy_client_take_message(vpc) == FALSE)
         goto failure;

      vpc->state = old_state;
   }

   return TRUE;

failure:
   return FALSE;
}

/* ----------------------------------------------------------------------------
** Start VBI acquisition, i.e. open connection to proxy daemon
*/
static vbi_bool proxy_client_start_acq( vbi_proxy_client * vpc )
{
   VBIPROXY_CONNECT_REQ * p_req_msg;
   VBIPROXY_CONNECT_CNF * p_cnf_msg;
   VBIPROXY_CONNECT_REJ * p_rej_msg;
   struct timeval tv;

   assert(vpc->state == CLNT_STATE_NULL);

   if (proxy_client_connect_server(vpc) == FALSE)
      goto failure;

   /* fake write request: make select to wait for socket to become writable */
   vpc->io.writeLen = 1;
   tv.tv_sec  = 4;
   tv.tv_usec = 0;

   /* wait for socket to reach connected state */
   if (proxy_client_wait_select(vpc, &tv) <= 0)
      goto failure;

   vpc->io.writeLen = 0;

   if (vbi_proxy_msg_finish_connect(vpc->io.sock_fd, &vpc->p_errorstr) == FALSE)
      goto failure;

   if (proxy_client_alloc_msg_buf(vpc) == FALSE)
      goto failure;

   /* write service request parameters */
   p_req_msg = &vpc->p_client_msg->body.connect_req;
   vbi_proxy_msg_fill_magics(&p_req_msg->magics);

   strncpy((char *) p_req_msg->client_name, vpc->p_client_name, VBIPROXY_CLIENT_NAME_MAX_LENGTH);
   p_req_msg->client_name[VBIPROXY_CLIENT_NAME_MAX_LENGTH - 1] = 0;
   p_req_msg->pid = getpid();

   p_req_msg->client_flags = vpc->client_flags;
   p_req_msg->scanning     = vpc->scanning;
   p_req_msg->services     = vpc->services;
   p_req_msg->strict       = vpc->strict;
   p_req_msg->buffer_count = vpc->buffer_count;

   /* send the connect request message to the proxy server */
   vbi_proxy_msg_write(&vpc->io, MSG_TYPE_CONNECT_REQ, sizeof(p_req_msg[0]),
                       vpc->p_client_msg, FALSE);

   vpc->state = CLNT_STATE_WAIT_CON_CNF;

   /* send message and wait for reply */
   if (proxy_client_rpc(vpc, MSG_TYPE_CONNECT_CNF, MSG_TYPE_CONNECT_REJ) == FALSE)
      goto failure;

   if (vpc->p_client_msg->head.type == MSG_TYPE_CONNECT_CNF)
   {
      p_cnf_msg = &vpc->p_client_msg->body.connect_cnf;

      /* first server message received: contains version info */
      /* note: nxtvepg and endian magics are already checked */
      if (p_cnf_msg->magics.protocol_compat_version != VBIPROXY_COMPAT_VERSION)
      {
         dprintf1("take_message: CONNECT_CNF: reply version %x, protocol %x\n", p_cnf_msg->magics.protocol_version, p_cnf_msg->magics.protocol_compat_version);

         asprintf (&vpc->p_errorstr,
		       _("Incompatible server version %u.%u.%u."),
		       ((p_cnf_msg->magics.protocol_compat_version >> 16) & 0xff),
		       ((p_cnf_msg->magics.protocol_compat_version >>  8) & 0xff),
		       ((p_cnf_msg->magics.protocol_compat_version      ) & 0xff));
         goto failure;
      }
      else if (vpc->endianSwap)
      {  /* endian swapping currently unsupported */
         asprintf(&vpc->p_errorstr, _("Incompatible server architecture (endianess mismatch)."));
         goto failure;
      }
      else
      {  /* version ok -> request block forwarding */
         dprintf1("Successfully connected to proxy (version %x.%x.%x, protocol %x.%x.%x)\n",
                  (p_cnf_msg->magics.protocol_version >> 16) & 0xff,
                  (p_cnf_msg->magics.protocol_version >> 8) & 0xff,
                  (p_cnf_msg->magics.protocol_version) & 0xff,
                  (p_cnf_msg->magics.protocol_compat_version >> 16) & 0xff,
                  (p_cnf_msg->magics.protocol_compat_version >> 8) & 0xff,
                  (p_cnf_msg->magics.protocol_compat_version) & 0xff);

         vpc->dec               = p_cnf_msg->dec;
         vpc->services          = p_cnf_msg->services;
         vpc->daemon_flags      = p_cnf_msg->daemon_flags;
         vpc->vbi_api_revision  = p_cnf_msg->vbi_api_revision;

         vpc->state = CLNT_STATE_CAPTURING;
      }
   }
   else
   {
      p_rej_msg = &vpc->p_client_msg->body.connect_rej;
      dprintf2("take_message: CONNECT_REJ: reply version %x, protocol %x\n", p_rej_msg->magics.protocol_version, p_rej_msg->magics.protocol_compat_version);
      if (vpc->p_errorstr != NULL)
      {
         free(vpc->p_errorstr);
         vpc->p_errorstr = NULL;
      }
      if (p_rej_msg->errorstr[0] != 0)
         vpc->p_errorstr = strdup((char *) p_rej_msg->errorstr);

      goto failure;
   }

   return TRUE;

failure:
   /* failed to establish a connection to the server */
   proxy_client_close(vpc);
   return FALSE;
}

/* ----------------------------------------------------------------------------
** Stop acquisition, i.e. close connection
*/
static void proxy_client_stop_acq( vbi_proxy_client * vpc )
{
   if (vpc->state != CLNT_STATE_NULL)
   {
      /* note: set the new state first to prevent callback from close function */
      vpc->state = CLNT_STATE_NULL;

      proxy_client_close(vpc);
   }
   else
      dprintf1("stop_acq: acq not enabled\n");
}

/* ----------------------------------------------------------------------------
** Process pending callbacks
** - returns FALSE if caller should return from loop
*/
static void
vbi_proxy_process_callbacks( vbi_proxy_client * vpc )
{
   VBI_PROXY_EV_TYPE ev_mask;

   if (vpc->ev_mask != VBI_PROXY_EV_NONE)
   {
      ev_mask = vpc->ev_mask;
      vpc->ev_mask = VBI_PROXY_EV_NONE;

      if (vpc->p_callback_func != NULL)
      {
         vpc->p_callback_func(vpc->p_callback_data, ev_mask);
      }
      else
      {
         if (ev_mask & VBI_PROXY_EV_CHN_RECLAIMED)
         {
         }
      }
   }
}

/* ----------------------------------------------------------------------------
**                  E X P O R T E D   F U N C T I O N S
** --------------------------------------------------------------------------*/

/* document below */
int
vbi_proxy_client_channel_request( vbi_proxy_client * vpc,
                                  VBI_CHN_PRIO chn_prio,
                                  vbi_channel_profile * p_chn_profile )
{
   VBIPROXY_CHN_TOKEN_REQ  * p_req;
   int result;

   if (vpc != NULL)
   {
      if (vpc->state == CLNT_STATE_ERROR)
         return -1;

      dprintf1("Request for channel token: prio=%d\n", chn_prio);
      assert(vpc->state == CLNT_STATE_CAPTURING);

      if (proxy_client_alloc_msg_buf(vpc) == FALSE)
         goto failure;

      /* wait for ongoing read to complete (XXX FIXME: don't discard messages) */
      if (proxy_client_wait_idle(vpc) == FALSE)
         goto failure;

      /* reset token in any case because prio or profile may have changed */
      vpc->has_token      = FALSE;
      vpc->ev_mask       &= ~VBI_PROXY_EV_CHN_GRANTED;
      vpc->chn_prio       = chn_prio;

      vpc->state          = CLNT_STATE_WAIT_RPC_REPLY;

      /* send channel change request to proxy daemon */
      p_req = &vpc->p_client_msg->body.chn_token_req;
      memset(p_req, 0, sizeof(p_req[0]));
      p_req->chn_prio    = chn_prio;
      p_req->chn_profile = *p_chn_profile;

      vbi_proxy_msg_write(&vpc->io, MSG_TYPE_CHN_TOKEN_REQ, sizeof(p_req[0]),
                          vpc->p_client_msg, FALSE);

      /* send message and wait for reply */
      if (proxy_client_rpc(vpc, MSG_TYPE_CHN_TOKEN_CNF, -1) == FALSE)
         goto failure;

      /* process reply message */
      vpc->has_token = vpc->p_client_msg->body.chn_token_cnf.token_ind;
      if (vpc->has_token)
      {
         vpc->ev_mask |= VBI_PROXY_EV_CHN_GRANTED;
      }

      vpc->state = CLNT_STATE_CAPTURING;
      result = (vpc->has_token ? 1 : 0);

      /* invoke callback in case TOKEN_IND was piggy-backed */
      vbi_proxy_process_callbacks(vpc);

      return result;
   }

failure:
   proxy_client_close(vpc);
   return -1;
}


/* document below */
int
vbi_proxy_client_channel_notify( vbi_proxy_client * vpc,
                                 VBI_PROXY_CHN_FLAGS notify_flags,
                                 unsigned int scanning )
{
   VBIPROXY_CHN_NOTIFY_REQ  * p_msg;

   if (vpc != NULL)
   {
      if (vpc->state == CLNT_STATE_ERROR)
         return -1;

      assert(vpc->state == CLNT_STATE_CAPTURING);

      if (proxy_client_alloc_msg_buf(vpc) == FALSE)
         goto failure;

      /* wait for ongoing read to complete (XXX FIXME: don't discard messages) */
      if (proxy_client_wait_idle(vpc) == FALSE)
         goto failure;

      dprintf1("Send channel notification: flags 0x%X, scanning %d (prio=%d, has_token=%d)\n", notify_flags, scanning, vpc->chn_prio, vpc->has_token);

      memset(vpc->p_client_msg, 0, sizeof(vpc->p_client_msg[0]));
      p_msg = &vpc->p_client_msg->body.chn_notify_req;

      p_msg->notify_flags = notify_flags;
      p_msg->scanning     = scanning;

      vbi_proxy_msg_write(&vpc->io, MSG_TYPE_CHN_NOTIFY_REQ, sizeof(p_msg[0]),
                          vpc->p_client_msg, FALSE);

      vpc->state = CLNT_STATE_WAIT_RPC_REPLY;

      /* send message and wait for reply */
      if (proxy_client_rpc(vpc, MSG_TYPE_CHN_NOTIFY_CNF, -1) == FALSE)
         goto failure;

      /* process reply message */
      /* XXX TODO */

      vpc->state = CLNT_STATE_CAPTURING;
   }

   /* invoke callback in case TOKEN_IND was piggy-backed */
   vbi_proxy_process_callbacks(vpc);

   return 0;

failure:
   proxy_client_close(vpc);
   return -1;
}


/* document below */
int
vbi_proxy_client_channel_suspend( vbi_proxy_client * vpc,
                                  VBI_PROXY_SUSPEND cmd )
{
   /* XXX TODO */

   vpc = vpc;
   cmd = cmd;

   return -1;
}


/* document below */
int
vbi_proxy_client_device_ioctl( vbi_proxy_client * vpc, int request, void * p_arg )
{
   VBIPROXY_MSG * p_msg;
   vbi_bool  req_perm;
   int  size;
   int  result = -1;

   if (vpc != NULL)
   {
      if (vpc->state == CLNT_STATE_CAPTURING)
      {
         /* determine size of the argument */
         size = vbi_proxy_msg_check_ioctl(vpc->vbi_api_revision, request, p_arg, &req_perm);
         if (size >= 0)
         {
            /* XXX TODO: for GET type calls on v4l2 use local device */

            if ( (req_perm == FALSE) ||
                 (vpc->chn_prio > VBI_CHN_PRIO_BACKGROUND) || vpc->has_token )
            {
               /* wait for ongoing read to complete (XXX FIXME: don't discard messages) */
               if (proxy_client_wait_idle(vpc) == FALSE)
                  goto failure;

               dprintf1("Forwarding ioctl: 0x%X, argp=0x%lX\n", request, (long)p_arg);

               p_msg = malloc(VBIPROXY_MSG_BODY_OFFSET + VBIPROXY_CHN_IOCTL_REQ_SIZE(size));
               if (p_msg == NULL)
                  goto failure;

               p_msg->body.chn_ioctl_req.request = request;
               p_msg->body.chn_ioctl_req.arg_size = size;
               if (size > 0)
                  memcpy(p_msg->body.chn_ioctl_req.arg_data, p_arg, size);

               vbi_proxy_msg_write(&vpc->io, MSG_TYPE_CHN_IOCTL_REQ,
                                   VBIPROXY_CHN_IOCTL_REQ_SIZE(size), p_msg, TRUE);

               /* send message and wait for reply */
               if (proxy_client_rpc(vpc, MSG_TYPE_CHN_IOCTL_CNF, MSG_TYPE_CHN_IOCTL_REJ) == FALSE)
                  goto failure;

               /* process reply message */
               if (vpc->p_client_msg->head.type == MSG_TYPE_CHN_IOCTL_CNF)
               {
                  if (size > 0)
                     memcpy(p_arg, vpc->p_client_msg->body.chn_ioctl_req.arg_data, size);
                  result = vpc->p_client_msg->body.chn_ioctl_cnf.result;
                  errno = vpc->p_client_msg->body.chn_ioctl_cnf.errcode;
               }
               else
               {
                  errno = EBUSY;
                  result = -1;
               }
               vpc->state = CLNT_STATE_CAPTURING;
            }
            else
            {
               dprintf1("vbi_proxy-client_ioctl: request not allowed without obtaining token first\n");
               errno = EBUSY;
            }
         }
         else
         {
            dprintf1("vbi_proxy-client_ioctl: unknown or not allowed request: 0x%X\n", request);
            errno = EINVAL;
         }
      }
      else
         dprintf1("vbi_proxy-client_ioctl: client in invalid state %d\n", vpc->state);

      vbi_proxy_process_callbacks(vpc);
   }
   else
      dprintf1("vbi_proxy-client_ioctl: invalid NULL ptr param\n");

failure:
   return result;
}


/* document below */
int
vbi_proxy_client_get_channel_desc( vbi_proxy_client * vpc,
                                   unsigned int * p_scanning,
                                   vbi_bool * p_granted )
{
   if (vpc != NULL)
   {
      if (p_scanning != NULL)
         *p_scanning = vpc->scanning;
      if (p_granted != NULL)
         *p_granted = vpc->has_token;

      return 0;
   }
   else
      return -1;
}


/* document below */
vbi_bool
vbi_proxy_client_has_channel_control( vbi_proxy_client * vpc )
{
   if (vpc != NULL)
   {
      return (vpc->has_token);
   }
   else
   {
      dprintf1("vbi_proxy_client-has_channel_token: NULL client param");
      return FALSE;
   }
}


/* document below */
VBI_DRIVER_API_REV
vbi_proxy_client_get_driver_api( vbi_proxy_client * vpc )
{
   if (vpc != NULL)
   {
      return vpc->vbi_api_revision;
   }
   else
      return VBI_API_UNKNOWN;
}


/* document below */
VBI_PROXY_CLIENT_CALLBACK *
vbi_proxy_client_set_callback( vbi_proxy_client * vpc,
                               VBI_PROXY_CLIENT_CALLBACK * p_callback, void * p_data )
{
   VBI_PROXY_CLIENT_CALLBACK * p_prev_cb = NULL;

   if (vpc != NULL)
   {
      p_prev_cb = vpc->p_callback_func;

      vpc->p_callback_func = p_callback;
      vpc->p_callback_data = p_data;
   }
   else
      dprintf1("vbi_proxy_client-set_callback: invalid pointer arg\n");

   return p_prev_cb;
}


/* document below */
vbi_capture *
vbi_proxy_client_get_capture_if( vbi_proxy_client * vpc )
{
   if (vpc != NULL)
   {
      return &vpc->capt_api;
   }
   else
      return NULL;
}

/* ----------------------------------------------------------------------------
**                  D E V I C E   C A P T U R E   A P I
** --------------------------------------------------------------------------*/

/**
 * @internal
 *
 * @param vpc Pointer to initialized proxy client context
 *
 * @return
 * Pointer to a vbi_raw_decoder structure, read only.  Returns @c NULL
 * upon error (i.e. if the client is not connected to the daemon)
 */
static vbi_raw_decoder *
vbi_proxy_client_get_dec_params( vbi_capture * vc )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);

   if (vc != NULL)
      return &vpc->dec;
   else
      return NULL;
}


/**
 * @internal
 *
 * @param vpc Pointer to initialized proxy client context
 *
 * @return
 * File descriptor of the socket used to connect to the proxy daemon or
 * -1 upon error (i.e. if the client is not connected to the daemon)
 * The descriptor can only be used for select() by caller, i.e. not for
 * read/write and must never be closed (call the close function instead)
 */
static int
vbi_proxy_client_get_fd( vbi_capture * vc )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);

   if (vc != NULL)
   {
      return vpc->io.sock_fd;
   }
   else
      return -1;
}


/**
 * @internal
 *
 * Read one frame's worth of VBI data.  If asynchronous events occur,
 * the callback is invoked before the call returns.
 *
 * Note: This function may indicate a timeout (i.e. return 0) even
 * if a previous select indicated readability. This will occur when
 * asynchronous messages (e.g. channel change indications) arrive.
 * Proxy clients should be prepared for this.  Channel change
 * indications can be supressed with VBI_PROXY_CLIENT_NO_STATUS_IND
 * in client flags during creation of the proxy, but there may still
 * be asynchronous messages when a token is granted.
 */
static int
vbi_proxy_client_read( vbi_capture * vc,
                       struct vbi_capture_buffer **pp_raw_buf,
                       struct vbi_capture_buffer **pp_slice_buf,
                       const struct timeval     * p_timeout )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);
   struct timeval timeout = *p_timeout;
   int  lines;
   int  result;

   if ((vc != NULL) && (vpc->state == CLNT_STATE_CAPTURING))
   {
      vpc->sliced_ind = FALSE;

      /* wait for message & read it (note: may also be some status ind) */
      result = proxy_client_read_message(vpc, &timeout);

      if (result > 0)
      {
         if (vpc->sliced_ind != FALSE)
         {
            if (pp_raw_buf != NULL)
            {
               lines = vpc->p_client_msg->body.sliced_ind.raw_lines;

               if (*pp_raw_buf != NULL)
               {
                  /* XXX optimization possible: read sliced msg into buffer to avoid memcpy */
                  memcpy( (*pp_raw_buf)->data,
                          vpc->p_client_msg->body.sliced_ind.u.raw,
                          lines * VBIPROXY_RAW_LINE_SIZE );
               }
               else
               {
                  *pp_raw_buf = &vpc->raw_buf;
                  (*pp_raw_buf)->data = vpc->p_client_msg->body.sliced_ind.u.raw;
               }
               (*pp_raw_buf)->size      = lines * VBIPROXY_RAW_LINE_SIZE;
               (*pp_raw_buf)->timestamp = vpc->p_client_msg->body.sliced_ind.timestamp;
            }

            if (pp_slice_buf != NULL)
            {
               lines = vpc->p_client_msg->body.sliced_ind.sliced_lines;

               if (*pp_slice_buf != NULL)
               {
                  /* XXX optimization possible: read sliced msg into buffer to avoid memcpy */
                  memcpy( (*pp_slice_buf)->data,
                          vpc->p_client_msg->body.sliced_ind.u.sliced,
                          lines * sizeof(vbi_sliced) );
               }
               else
               {
                  *pp_slice_buf = &vpc->slice_buf;
                  (*pp_slice_buf)->data = vpc->p_client_msg->body.sliced_ind.u.sliced;
               }

               (*pp_slice_buf)->size      = lines * sizeof(vbi_sliced);
               (*pp_slice_buf)->timestamp = vpc->p_client_msg->body.sliced_ind.timestamp;
            }
         }
         else
         {  /* not a slicer data unit */
            result = 0;
         }
         vbi_proxy_process_callbacks(vpc);
      }
      return result;
   }
   errno = EBADF;
   return -1;
}


/**
 * @internal
 *
 * Add and/or remove one or more services to an already initialized
 * capture context.
 *
 * Note the "commit" parameter is currently not applicable to proxy clients.
 */
static unsigned int
vbi_proxy_client_update_services( vbi_capture * vc,
                                  vbi_bool reset, vbi_bool commit,
                                  unsigned int services, int strict,
                                  char ** pp_errorstr )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);

   if (vc != NULL)
   {
      if (vpc->state == CLNT_STATE_ERROR)
         return 0;

      assert(vpc->state == CLNT_STATE_CAPTURING);

      if (proxy_client_alloc_msg_buf(vpc) == FALSE)
         goto failure;

      /* wait for ongoing read to complete */
      if (proxy_client_wait_idle(vpc) == FALSE)
         goto failure;

      vpc->state = CLNT_STATE_WAIT_SRV_CNF;

      dprintf1("update_services: send service req: srv %d, strict %d\n", services, strict);

      /* send service request to proxy daemon */
      vpc->p_client_msg->body.service_req.reset        = reset;
      vpc->p_client_msg->body.service_req.commit       = commit;
      vpc->p_client_msg->body.service_req.services     = services;
      vpc->p_client_msg->body.service_req.strict       = strict;
      vbi_proxy_msg_write(&vpc->io, MSG_TYPE_SERVICE_REQ, sizeof(vpc->p_client_msg->body.service_req),
                          vpc->p_client_msg, FALSE);

      /* send message and wait for reply */
      if (proxy_client_rpc(vpc, MSG_TYPE_SERVICE_CNF, MSG_TYPE_SERVICE_REJ) == FALSE)
         goto failure;

      if (vpc->p_client_msg->head.type == MSG_TYPE_SERVICE_CNF)
      {
         memset(&vpc->dec, 0, sizeof(vpc->dec));

         vpc->services = vpc->p_client_msg->body.service_cnf.services;
         memcpy(&vpc->dec, &vpc->p_client_msg->body.service_cnf.dec, sizeof(vpc->dec));
         dprintf1("service cnf: granted service %d\n", vpc->dec.services);
      }
      else
      {
         /* process the message */
         if ( (vpc->p_client_msg->body.service_rej.errorstr[0] != 0) &&
              (pp_errorstr != NULL) )
         {
            *pp_errorstr = strdup((char *) vpc->p_client_msg->body.service_rej.errorstr);
         }
      }
      vpc->state = CLNT_STATE_CAPTURING;

      return services & vpc->services;
   }

failure:
   if (vpc->p_errorstr != NULL)
   {
      if (pp_errorstr != NULL)
         *pp_errorstr = vpc->p_errorstr;
      else
         free(vpc->p_errorstr);
      vpc->p_errorstr = NULL;
   }
   proxy_client_close(vpc);
   return 0;
}


/**
 * @internal
 *
 * Note this function is only present because it's part of the capture
 * device API.  Proxy-aware clients should use the proxy client API
 * function vbi_proxy_client_channel_notify() instead of this one, because
 * it allows to return the channel control "token" at the same time.
 */
static void
vbi_proxy_client_flush( vbi_capture * vc )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);

   if (vc != NULL)
   {
      vbi_proxy_client_channel_notify(vpc, VBI_PROXY_CHN_FLUSH, 0);
   }
}


/**
 * @internal
 *
 * @param vpc Pointer to initialized proxy client context
 *
 * Queries properties of the exported "capture device" file handle.
 */
static VBI_CAPTURE_FD_FLAGS
vbi_proxy_client_get_fd_flags(vbi_capture *vc)
{
	vc = vc;

        return VBI_FD_HAS_SELECT;
}

/**
 * @internal
 *
 * @param vpc Pointer to initialized proxy client context
 *
 * Close connection to the proxy daemon.  The proxy client context
 * can be re-used for another connection later.
 */
static void
vbi_proxy_client_stop( vbi_capture * vc )
{
   vbi_proxy_client * vpc = PARENT(vc, vbi_proxy_client, capt_api);

   if (vc != NULL)
   {
      proxy_client_stop_acq(vpc);
   }
   if (vpc->ctrl_fd != -1)
   {
      close(vpc->ctrl_fd);
      vpc->ctrl_fd = -1;
   }
}

/* document below */
vbi_capture *
vbi_capture_proxy_new( struct vbi_proxy_client * vpc,
                       int buffers, int scanning,
                       unsigned int *p_services, int strict,
                       char **pp_errorstr )
{
   if (vpc != NULL)
   {
      if ( (vpc->state == CLNT_STATE_NULL) ||
           (vpc->state == CLNT_STATE_ERROR) )
      {
         if (scanning != 525 && scanning != 625)
            scanning = 0;

         if (buffers < 1)
            buffers = 1;

         if (strict < -1)
            strict = -1;
         else if (strict > 2)
            strict = 2;

         /* check and copy parameters into state struct */
         assert((p_services == NULL) || (*p_services != 0));

         vpc->buffer_count = buffers;
         vpc->scanning     = scanning,
         vpc->services     = ((p_services != NULL) ? *p_services : 0);
         vpc->strict       = strict;

         /* reset state if in error state (e.g. previous connect failed) */
         vpc->state = CLNT_STATE_NULL;

         /* send params to daemon and wait for reply */
         if ( proxy_client_start_acq(vpc) )
         {
            assert(vpc->state == CLNT_STATE_CAPTURING);
            assert((p_services == NULL) || (vpc->services != 0));

            if (p_services != NULL)
               *p_services = vpc->services;

            return &vpc->capt_api;
         }
      }
      else
         dprintf1("vbi_proxy-client_start: illegal state %d for start\n", vpc->state);
   }
   else
      dprintf1("vbi_proxy-client_start: illegal NULL ptr param\n");

   if (pp_errorstr != NULL)
      *pp_errorstr = vpc->p_errorstr;
   else
      free(vpc->p_errorstr);
   vpc->p_errorstr = NULL;

   return NULL;
}

void
vbi_proxy_client_destroy( vbi_proxy_client * vpc )
{
   if (vpc != NULL)
   {
      /* close the connection (during normal shutdown it should already be closed) */
      if (vpc->state != CLNT_STATE_NULL)
         proxy_client_stop_acq(vpc);

      if (vpc->ctrl_fd != -1)
         close(vpc->ctrl_fd);

      if (vpc->p_srv_host != NULL)
         free(vpc->p_srv_host);

      if (vpc->p_srv_port != NULL)
         free(vpc->p_srv_port);

      if (vpc->p_client_msg != NULL)
         free(vpc->p_client_msg);

      if (vpc->p_errorstr != NULL)
         free(vpc->p_errorstr);

      free(vpc);
   }
}

/* document below */
vbi_proxy_client *
vbi_proxy_client_create( const char *p_dev_name, const char *p_client_name,
                         VBI_PROXY_CLIENT_FLAGS client_flags,
                         char **pp_errorstr, int trace_level )
{
   vbi_proxy_client * vpc;

   if (trace_level)
   {
      fprintf(stderr, "Creating vbi proxy client, rev.\n%s\n", rcsid);
      vbi_proxy_msg_set_debug_level(trace_level);
   }

   vpc = (vbi_proxy_client *) calloc(1, sizeof(vpc[0]));
   if (vpc != NULL)
   {
      /* fill capture interface struct */
      vpc->capt_api.parameters = vbi_proxy_client_get_dec_params;
      vpc->capt_api._delete = vbi_proxy_client_stop;
      vpc->capt_api.get_fd = vbi_proxy_client_get_fd;
      vpc->capt_api.get_fd_flags = vbi_proxy_client_get_fd_flags;
      vpc->capt_api.read = vbi_proxy_client_read;
      vpc->capt_api.update_services = vbi_proxy_client_update_services;
      vpc->capt_api.flush = vbi_proxy_client_flush;

      /* initialize client state with given parameters */
      vpc->p_client_name = strdup(p_client_name);
      vpc->client_flags  = client_flags;
      vpc->p_srv_port    = vbi_proxy_msg_get_socket_name(p_dev_name);
      vpc->p_srv_host    = NULL;
      vpc->trace         = trace_level;

      vpc->state         = CLNT_STATE_NULL;
      vpc->ctrl_fd       = -1;
   }
   else
   {
      asprintf(pp_errorstr, _("Virtual memory exhausted."));
   }
   return vpc;
}

#else /* !ENABLE_PROXY */

/**
 * @addtogroup Proxy VBI capture proxy interface
 * @ingroup Raw
 * @brief Receiving sliced or raw data from VBI proxy daemon
 *
 * Using the VBI proxy daemon instead of capturing directly from a
 * VBI device allows multiple clients to capture concurrently, e.g.
 * to decode multiple data services.
 */

/**
 * @param vpc Pointer to initialized proxy client context
 * @param chn_prio Channel change priority level.  If there are other clients
 *   with higher priority the client will be refused any channel changes.
 * @param p_chn_profile Channel profile for scheduling at background
 *   priority level.
 *
 * This function is used to request permission to switch channels or norm.
 * Since the VBI device can be shared with other proxy clients, clients should
 * wait for permission, so that the proxy daemon can fairly schedule channel
 * requests.
 *
 * Scheduling differs at the 3 priority levels. For an explanation of
 * priorities see enum VBI_CHN_PRIO.  At background level channel changes
 * are coordinated by introduction of a virtual token: only the
 * one client which holds the token is allowed to switch channels. The daemon
 * will wait for the token to be returned before it's granted to another
 * client.  This way conflicting channel changes are avoided.
 *
 * At the upper level the latest request always wins.  To avoid interference
 * the application still might wait until it gets indicated that the token
 * has been returned to the daemon.
 *
 * The token may be granted right away or at a later time, e.g. when it has
 * to be reclaimed from another client first, or if there are other clients
 * with higher priority.  If a callback has been registered, it will be
 * invoked when the token arrives; otherwise
 * vbi_proxy_client_has_channel_control()
 * can be used to poll for it.
 *
 * Note: to set the priority level to "background" only without requesting
 * a channel, set the is_valid member in the profile to @c FALSE.
 *
 * @return
 * 1 if change is allowed, 0 if not allowed,
 * -1 on error, examine @c errno for details.
 *
 * @since 0.2.9
 */
/* XXX TODO improve description */
int
vbi_proxy_client_channel_request( vbi_proxy_client * vpc,
                                  VBI_CHN_PRIO chn_prio,
                                  vbi_channel_profile * p_chn_profile )
{
   errno = 0;
   return -1;
}


/**
 * @param vpc Pointer to initialized proxy client context
 * @param notify_flags Combination of event notification bits
 * @param scanning New norm, if norm event bit is set
 *
 * Send channel control request to proxy daemon.
 * See description of the flags for details.
 *
 * @return
 * 0 upon success, -1 on error, examine @c errno for details.
 *
 * @since 0.2.9
 */
int
vbi_proxy_client_channel_notify( vbi_proxy_client * vpc,
                                 VBI_PROXY_CHN_FLAGS notify_flags,
                                 unsigned int scanning )
{
   return -1;
}


/**
 * @param vpc Pointer to initialized proxy client context
 * @param cmd Control command
 *
 * Request to temporarily suspend capturing
 *
 * @return
 * 0 upon success, -1 on error, examine @c errno for details.
 *
 * @since 0.2.9
 */
int
vbi_proxy_client_channel_suspend( vbi_proxy_client * vpc,
                                  VBI_PROXY_SUSPEND cmd )
{
   return -1;
}


/**
 * @param vpc Pointer to initialized proxy client context
 * @param request Ioctl request code to be passed to driver
 * @param p_arg Ioctl argument to be passed to driver
 *
 * @brief Wrapper for ioctl requests on the VBI device
 *
 * This function allows to manipulate parameters of the underlying
 * VBI device.  Not all ioctls are allowed here.  It's mainly intended
 * to be used for channel enumeration and channel/norm changes.  
 * The request codes and parameters are the same as for the actual device.
 * The caller has to query the driver API first and use the respective
 * ioctl codes, same as if the device would be used directly.
 *
 * @return
 * Same as for the ioctl, i.e. -1 on error and errno set appropriately.
 * The funtion also will fail with errno @c EBUSY if the client doesn't
 * have permission to control the channel.
 *
 * @since 0.2.9
 */
int
vbi_proxy_client_device_ioctl( vbi_proxy_client * vpc,
			       int request, void * p_arg )
{
   return -1;
}

/**
 * @param vpc Pointer to initialized proxy client context
 * @param p_scanning Returns new scanning after channel change
 * @param p_granted Returns@c TRUE if client is currently allowed to
 *   switch channels
 *
 * Retrieve info sent by the proxy daemon in a channel change indication.
 *
 * @return
 * 0 upon success, -1 on error.
 *
 * @since 0.2.9
 */
int
vbi_proxy_client_get_channel_desc( vbi_proxy_client * vpc,
                                   unsigned int * p_scanning,
                                   vbi_bool * p_granted )
{
   return -1;
}

/**
 * @param vpc Pointer to initialized proxy client context
 *
 * @brief Query if the client is currently allowed to switch channels
 *
 * @return
 * Returns @c TRUE if client is currently allowed to switch channels.
 *
 * @since 0.2.9
 */
vbi_bool
vbi_proxy_client_has_channel_control( vbi_proxy_client * vpc )
{
   return FALSE;
}

/**
 * @param vpc Pointer to initialized proxy client context
 *
 * @brief Returns the driver type behind the actual capture device
 *
 * This function can be used to query which driver is behind the
 * device which is currently opened by the VBI proxy daemon.
 * Applications which use libzvbi's capture API only need not
 * care about this.  The information is only relevant to applications
 * which need to change channels or norms.
 *
 * The function will fail if the client is currently not connected
 * to the daemon, i.e. VPI capture has to be started first.
 *
 * @return
 * Driver type or -1 on error.
 *
 * @since 0.2.9
 */
VBI_DRIVER_API_REV
vbi_proxy_client_get_driver_api( vbi_proxy_client * vpc )
{
   return VBI_API_UNKNOWN;
}

/**
 * @param vpc Pointer to initialized proxy client context
 * @param p_callback Pointer to callback function
 * @param p_data Void pointer which will be passed through to the
 *   callback function unmodified.
 *
 * @brief Installs callback function for asynchronous events
 *
 * This function installs a callback function which will be invoked
 * upon asynchronous events (e.g. channel changes by other clients.)
 * Since the proxy client has no activity carrier on it's own (i.e.
 * it's not using an internal thread or process) callbacks will only
 * occur from inside other proxy client function calls.  The client's
 * file description will become readable when an asynchronous message
 * has arrived from the daemon.  Typically the application then will
 * call read to obtain sliced data and the callback will be invoked
 * from inside the read function.  Usually in this case the read call
 * will return zero, i.e. indicate an timeout since no actual sliced
 * data has arrived.
 *
 * Note for channel requests the callback to grant channel control may
 * be invoked before the request function returns.
 * Note you can call any interface function from inside the callback,
 * including the destroy operator.
 *
 * @return
 * Returns pointer to the previous callback or @c NULL if none.
 *
 * @since 0.2.9
 */
VBI_PROXY_CLIENT_CALLBACK *
vbi_proxy_client_set_callback( vbi_proxy_client * vpc,
                               VBI_PROXY_CLIENT_CALLBACK * p_callback,
			       void * p_data )
{
   return NULL;
}

/**
 * @param vpc Pointer to initialized and active proxy client context
 *
 * @brief Returns capture interface for an initialized proxy client
 *
 * This function is for convenience only: it returns the same pointer
 * as the previous call to vbi_capture_proxy_new(), so that the client
 * need not store it.  This pointer is required for function calls
 * through the capture device API (e.g. reading raw or sliced data)
 *
 * @return
 * Pointer to a vbi_capture structure, should be treated as void * by
 * caller, i.e. acessed neither for read nor write.  Returns @c NULL
 * upon error (i.e. if the client is not connected to the daemon)
 *
 * @since 0.2.9
 */
vbi_capture *
vbi_proxy_client_get_capture_if( vbi_proxy_client * vpc )
{
   return NULL;
}

/**
 * @ingroup Device
 *
 * @param p_proxy_client Reference to an initialized proxy client
 *   context.
 * @param buffers Number of device buffers for raw vbi data. The same
 *   number of buffers is allocated to cache sliced data in the proxy daemon.
 * @param scanning This indicates the current norm: 625 for PAL and
 *   525 for NTSC; set to 0 if you don't know (you should not attempt
 *   to query the device for the norm, as this parameter is only required
 *   for v4l1 drivers which don't support video standard query ioctls)
 * @param p_services This must point to a set of @ref VBI_SLICED_
 *   symbols describing the
 *   data services to be decoded. On return the services actually
 *   decodable will be stored here. See vbi_raw_decoder_add()
 *   for details. If you want to capture raw data only, set to
 *   @c VBI_SLICED_VBI_525, @c VBI_SLICED_VBI_625 or both.
 * @param strict Will be passed to vbi_raw_decoder_add().
 * @param pp_errorstr If not @c NULL this function stores a pointer to an error
 *   description here. You must free() this string when no longer needed.
 *
 * Open a new connection to a VBI proxy to open a VBI device for the
 * given services.  On side of the proxy one of the regular v4l_new()
 * functions is invoked and if it succeeds, data slicing is started
 * and all captured data forwarded transparently.
 *
 * Whenever possible the proxy should be used instead of opening the device
 * directly, since it allows the user to start multiple VBI clients in
 * parallel.  When this function fails (usually because the user hasn't
 * started the proxy daemon) applications should automatically fall back
 * to opening the device directly.
 *
 * @return
 * Initialized vbi_capture context, @c NULL on failure.
 *
 * @since 0.2.9
 */
vbi_capture *
vbi_capture_proxy_new( struct vbi_proxy_client *p_proxy_client,
                       int buffers, int scanning,
                       unsigned int *p_services, int strict,
                       char **pp_errorstr )
{
   pthread_once (&vbi_init_once, vbi_init);
   asprintf(pp_errorstr, _("Proxy client interface not compiled."));
   return NULL;
}

/**
 * @param vpc Pointer to initialized proxy client context
 *
 * This function closes the connection to the proxy daemon and frees
 * all resources.  The given context must no longer be used after this
 * function was called.  If the context was used via the capture device
 * interface, the vbi_capture context must be destroyed first.
 *
 * @since 0.2.9
 */
void
vbi_proxy_client_destroy( vbi_proxy_client * vpc )
{
   vpc = vpc;
}

/**
 * @param p_dev_name Name of the device to open, usually one of
 *   @c /dev/vbi or @c /dev/vbi0 and up.  Note: should be the same path as
 *   used by the proxy daemon, else the client may not be able to connect.
 * @param p_client_name Name of the client application, typically identical
 *   to argv[0] (without the path though)  Can be used by the proxy daemon
 *   to fine-tune scheduling or to present the user with a list of
 *   currently connected applications.
 * @param client_flags Can contain one or more members of
 * VBI_PROXY_CLIENT_FLAGS
 * @param pp_errorstr If not @c NULL this function stores a pointer to an error
 *   description here. You must free() this string when no longer needed.
 * @param trace_level Enable debug output to stderr if non-zero.
 *   Larger values produce more output.
 *
 * This function initializes a proxy daemon client context with the given
 * parameters.  (Note this function does not yet connect the daemon.)
 *
 * @return
 * Initialized proxy client context, @c NULL on failure
 *
 * @since 0.2.9
 */
vbi_proxy_client *
vbi_proxy_client_create(const char *p_dev_name, const char *p_client_name,
                        VBI_PROXY_CLIENT_FLAGS client_flags,
                        char **pp_errorstr, int trace_level)
{
   asprintf(pp_errorstr, _("Proxy client interface not compiled."));
   return NULL;
}

#endif /* !ENABLE_PROXY */