/*
 * CORE
 * Copyright (c)2010-2012 the Boeing Company.
 * See the LICENSE file included in this distribution.
 *
 * author: Tom Goff <thomas.goff@boeing.com>
 *
 * vnode_client.c
 *
 *
 *
 */

#include <stdio.h>
#include <limits.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>

#include "vnode_chnl.h"
#include "vnode_client.h"
#include "vnode_tlv.h"
#include "vnode_io.h"


extern int verbose;

typedef struct {
  vnode_client_cmddonecb_t cmddonecb;
  void *data;
} vnode_clientcmd_t;

vnode_client_cmdio_t *vnode_open_clientcmdio(vnode_client_cmdiotype_t iotype)
{
  int err;
  vnode_client_cmdio_t *clientcmdio;

  clientcmdio = malloc(sizeof(*clientcmdio));
  if (!clientcmdio)
  {
    WARN("malloc() failed");
    return NULL;
  }

  clientcmdio->iotype = iotype;

  switch (clientcmdio->iotype)
  {
  case VCMD_IO_NONE:
  case VCMD_IO_FD:
    err = 0;
    break;

  case VCMD_IO_PIPE:
    err = open_stdio_pipe(&clientcmdio->stdiopipe);
    break;

  case VCMD_IO_PTY:
    err = open_stdio_pty(&clientcmdio->stdiopty);
    break;

  default:
    WARNX("unknown i/o type: %u", clientcmdio->iotype);
    err = -1;
    break;
  }

  if (err)
  {
    free(clientcmdio);
    clientcmdio = NULL;
  }

  return clientcmdio;
}

void vnode_close_clientcmdio(vnode_client_cmdio_t *clientcmdio)
{
  switch (clientcmdio->iotype)
  {
  case VCMD_IO_NONE:
  case VCMD_IO_FD:
    break;

  case VCMD_IO_PIPE:
    close_stdio_pipe(&clientcmdio->stdiopipe);
    break;

  case VCMD_IO_PTY:
    close_stdio_pty(&clientcmdio->stdiopty);
    break;

  default:
    WARNX("unknown i/o type: %u", clientcmdio->iotype);
    break;
  }

  memset(clientcmdio, 0, sizeof(*clientcmdio));
  free(clientcmdio);

  return;
}

static void vnode_client_cmddone(vnode_cmdentry_t *cmd)
{
  vnode_clientcmd_t *clientcmd = cmd->data;;

  if (clientcmd->cmddonecb)
    clientcmd->cmddonecb(cmd->cmdid, cmd->pid, cmd->status, clientcmd->data);

  memset(clientcmd, 0, sizeof(*clientcmd));
  free(clientcmd);

  memset(cmd, 0, sizeof(*cmd));
  free(cmd);

  return;
}

static int tlv_cmdreqack_cmdid(vnode_tlv_t *tlv, void *data)
{
  vnode_cmdreqack_t *cmdreqack = data;
  int tmp;

  assert(tlv->type == VNODE_TLV_CMDID);

  tmp = tlv_int32(&cmdreqack->cmdid, tlv);

  if (tmp == 0 && verbose)
    INFO("VNODE_TLV_CMDID: %d", cmdreqack->cmdid);

  return tmp;
}

static int tlv_cmdreqack_cmdpid(vnode_tlv_t *tlv, void *data)
{
  vnode_cmdreqack_t *cmdreqack = data;
  int tmp;

  assert(tlv->type == VNODE_TLV_CMDPID);

  tmp = tlv_int32(&cmdreqack->pid, tlv);

  if (tmp == 0 && verbose)
    INFO("VNODE_TLV_CMDPID: %d", cmdreqack->pid);

  return tmp;
}

static void vnode_clientrecv_cmdreqack(vnode_msgio_t *msgio)
{
  vnode_cmdentry_t *cmd;
  vnode_client_t *client = msgio->data;
  vnode_cmdreqack_t cmdreqack = CMDREQACK_INIT;
  static const vnode_tlvhandler_t tlvhandler[VNODE_TLV_MAX] = {
    [VNODE_TLV_CMDID] = tlv_cmdreqack_cmdid,
    [VNODE_TLV_CMDPID] = tlv_cmdreqack_cmdpid,
  };

#ifdef DEBUG
  WARNX("command request ack");
#endif

  assert(msgio->msgbuf.msg->hdr.type == VNODE_MSG_CMDREQACK);

  if (vnode_parsemsg(msgio->msgbuf.msg, &cmdreqack, tlvhandler))
    return;

  TAILQ_FOREACH(cmd, &client->cmdlisthead, entries)
    if (cmd->cmdid == cmdreqack.cmdid)
      break;

  if (cmd == NULL)
  {
    WARNX("cmdid %d not found in command list", cmdreqack.cmdid);
    return;
  }

#ifdef DEBUG
  WARNX("cmdid %d found in cmd list", cmdreqack.cmdid);
#endif

  cmd->pid = cmdreqack.pid;

  if (cmdreqack.pid == -1)
  {
#ifdef DEBUG
    WARNX("XXX pid == -1 removing cmd from list");
#endif
    TAILQ_REMOVE(&client->cmdlisthead, cmd, entries);

    cmd->status = -1;
    vnode_client_cmddone(cmd);

    return;
  }

  return;
}

static int tlv_cmdstatus_cmdid(vnode_tlv_t *tlv, void *data)
{
  vnode_cmdstatus_t *cmdstatus = data;
  int tmp;

  assert(tlv->type == VNODE_TLV_CMDID);

  tmp = tlv_int32(&cmdstatus->cmdid, tlv);

  if (tmp == 0 && verbose)
    INFO("VNODE_TLV_CMDID: %d", cmdstatus->cmdid);

  return tmp;
}

static int tlv_cmdstatus_status(vnode_tlv_t *tlv, void *data)
{
  vnode_cmdstatus_t *cmdstatus = data;
  int tmp;

  assert(tlv->type == VNODE_TLV_CMDSTATUS);

  tmp = tlv_int32(&cmdstatus->status, tlv);

  if (tmp == 0 && verbose)
    INFO("VNODE_TLV_CMDSTATUS: %d", cmdstatus->status);

  return tmp;
}

static void vnode_clientrecv_cmdstatus(vnode_msgio_t *msgio)
{
  vnode_cmdentry_t *cmd;
  vnode_client_t *client = msgio->data;
  vnode_cmdstatus_t cmdstatus = CMDSTATUS_INIT;
  static const vnode_tlvhandler_t tlvhandler[VNODE_TLV_MAX] = {
    [VNODE_TLV_CMDID] = tlv_cmdstatus_cmdid,
    [VNODE_TLV_CMDSTATUS] = tlv_cmdstatus_status,
  };

#ifdef DEBUG
  WARNX("command status");
#endif

  assert(msgio->msgbuf.msg->hdr.type == VNODE_MSG_CMDSTATUS);

  if (vnode_parsemsg(msgio->msgbuf.msg, &cmdstatus, tlvhandler))
    return;

  TAILQ_FOREACH(cmd, &client->cmdlisthead, entries)
    if (cmd->cmdid == cmdstatus.cmdid)
      break;

  if (cmd == NULL)
  {
    WARNX("cmdid %d not found in command list", cmdstatus.cmdid);
    return;
  }

#ifdef DEBUG
  WARNX("cmdid %d found in cmd list; removing", cmdstatus.cmdid);
#endif
  TAILQ_REMOVE(&client->cmdlisthead, cmd, entries);

  cmd->status = cmdstatus.status;
  vnode_client_cmddone(cmd);

  return;
}

static void server_ioerror(vnode_msgio_t *msgio)
{
  vnode_client_t *client = msgio->data;

#ifdef DEBUG
  WARNX("i/o error on fd %d; client: %p", msgio->fd, client);
#endif

  if (client)
  {
    assert(msgio == &client->msgio);
    if (client->ioerrorcb)
      client->ioerrorcb(client);
  }

  return;
}

vnode_client_t *vnode_client(struct ev_loop *loop, const char *ctrlchnlname,
			     vnode_clientcb_t ioerrorcb, void *data)
{
  int fd = -1;
  vnode_client_t *client;
  static const vnode_msghandler_t msghandler[VNODE_MSG_MAX] = {
    [VNODE_MSG_CMDREQACK] = vnode_clientrecv_cmdreqack,
    [VNODE_MSG_CMDSTATUS] = vnode_clientrecv_cmdstatus,
  };

  if (!ioerrorcb)
  {
    WARNX("no i/o error callback given");
    return NULL;
  }

  fd = vnode_connect(ctrlchnlname);
  if (fd < 0)
  {
    WARN("vnode_connect() failed for '%s'", ctrlchnlname);
    return NULL;
  }

  if ((client = calloc(1, sizeof(*client))) == NULL)
  {
    WARN("calloc() failed");
    close(fd);
    return NULL;
  }

  TAILQ_INIT(&client->cmdlisthead);
  client->loop = loop;
  client->serverfd = fd;
  client->ioerrorcb = ioerrorcb;
  client->data = data;

  if (vnode_msgiostart(&client->msgio,  client->loop,
		       client->serverfd, client, server_ioerror, msghandler))
  {
    WARNX("vnode_msgiostart() failed");
    close(fd);
    return NULL;
  }

#ifdef DEBUG
  WARNX("new client connected to %s: %p", ctrlchnlname, client);
#endif

  return client;
}

void vnode_delclient(vnode_client_t *client)
{
#ifdef DEBUG
  WARNX("deleting client: %p", client);
#endif

  vnode_msgiostop(&client->msgio);
  if (client->serverfd >= 0)
  {
    close(client->serverfd);
    client->serverfd = -1;
  }

  while (!TAILQ_EMPTY(&client->cmdlisthead))
  {
    vnode_cmdentry_t *cmd;

    cmd = TAILQ_FIRST(&client->cmdlisthead);
    TAILQ_REMOVE(&client->cmdlisthead, cmd, entries);

    cmd->status = -1;
    vnode_client_cmddone(cmd);
  }

  /* XXX more stuff ?? */

  memset(client, 0, sizeof(*client));
  free(client);

  return;
}

static int vnode_setcmdio(int *cmdin, int *cmdout, int *cmderr,
			  vnode_client_cmdio_t *clientcmdio)
{
  switch (clientcmdio->iotype)
  {
  case VCMD_IO_NONE:
    *cmdin = -1;
    *cmdout = -1;
    *cmderr = -1;
    break;

  case VCMD_IO_FD:
    *cmdin = clientcmdio->stdiofd.infd;
    *cmdout = clientcmdio->stdiofd.outfd;
    *cmderr = clientcmdio->stdiofd.errfd;
    break;

  case VCMD_IO_PIPE:
    *cmdin = clientcmdio->stdiopipe.infd[0];
    *cmdout = clientcmdio->stdiopipe.outfd[1];
    *cmderr = clientcmdio->stdiopipe.errfd[1];
    break;

  case VCMD_IO_PTY:
    *cmdin = clientcmdio->stdiopty.slavefd;
    *cmdout = clientcmdio->stdiopty.slavefd;
    *cmderr = clientcmdio->stdiopty.slavefd;
    break;

  default:
    WARNX("unknown i/o type: %u", clientcmdio->iotype);
    return -1;
  }

  return 0;
}

static void vnode_cleanupcmdio(vnode_client_cmdio_t *clientcmdio)
{
#define CLOSE(var)				\
  do {						\
    if (var >= 0)				\
      close(var);				\
    var = -1;					\
  } while (0)

  switch (clientcmdio->iotype)
  {
  case VCMD_IO_NONE:
  case VCMD_IO_FD:
    break;

  case VCMD_IO_PIPE:
    CLOSE(clientcmdio->stdiopipe.infd[0]);
    CLOSE(clientcmdio->stdiopipe.outfd[1]);
    CLOSE(clientcmdio->stdiopipe.errfd[1]);
    break;

  case VCMD_IO_PTY:
    CLOSE(clientcmdio->stdiopty.slavefd);
    break;

  default:
    WARNX("unknown i/o type: %u", clientcmdio->iotype);
    break;
  }

#undef CLOSE

  return;
}

int vnode_client_cmdreq(vnode_client_t *client,
			vnode_client_cmdio_t *clientcmdio,
			vnode_client_cmddonecb_t cmddonecb, void *data,
			int argc, char *argv[])
{
  int cmdin, cmdout, cmderr;
  vnode_clientcmd_t *clientcmd;
  vnode_cmdentry_t *cmd;

  if (argc >= VNODE_ARGMAX)
  {
    WARNX("too many command arguments");
    return -1;
  }

  if (argv[argc] != NULL)
  {
    WARNX("command arguments not null-terminated");
    return -1;
  }

  if (vnode_setcmdio(&cmdin, &cmdout, &cmderr, clientcmdio))
  {
    WARNX("vnode_setcmdio() failed");
    return -1;
  }

  if ((clientcmd = malloc(sizeof(*clientcmd))) == NULL)
  {
    WARN("malloc() failed");
    return -1;
  }

  clientcmd->cmddonecb = cmddonecb;
  clientcmd->data = data;

  if ((cmd = malloc(sizeof(*cmd))) == NULL)
  {
    WARN("malloc() failed");
    free(clientcmd);
    return -1;
  }

  if (client->cmdid < 0)
    client->cmdid = 0;
  cmd->cmdid = client->cmdid++;
  cmd->pid = -1;
  cmd->status = -1;
  cmd->data = clientcmd;

  TAILQ_INSERT_TAIL(&client->cmdlisthead, cmd, entries);

  if (vnode_send_cmdreq(client->serverfd, cmd->cmdid,
			argv, cmdin, cmdout, cmderr))
  {
    WARN("vnode_send_cmdreq() failed");
    TAILQ_REMOVE(&client->cmdlisthead, cmd, entries);
    free(clientcmd);
    free(cmd);
    return -1;
  }

  vnode_cleanupcmdio(clientcmdio);

  return cmd->cmdid;
}