/* libguestfs Erlang bindings.
 * Copyright (C) 2011-2017 Red Hat Inc.
 *
 * 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.
 *
 * 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>

#include <erl_interface.h>
/* We should switch over to using
  #include <ei.h>
instead of erl_interface.
*/

#include "error.h"
#include "full-read.h"
#include "full-write.h"

#include "guestfs.h"
#include "guestfs-internal-frontend.h"

guestfs_h *g;

extern ETERM *dispatch (ETERM *message);
extern int atom_equals (ETERM *atom, const char *name);
extern ETERM *make_error (const char *funname);
extern ETERM *unknown_optarg (const char *funname, ETERM *optargname);
extern ETERM *unknown_function (ETERM *fun);
extern ETERM *make_string_list (char **r);
extern ETERM *make_table (char **r);
extern ETERM *make_bool (int r);
extern char **get_string_list (ETERM *term);
extern int get_bool (ETERM *term);
extern int get_int (ETERM *term);
extern int64_t get_int64 (ETERM *term);

/* This stops things getting out of hand, but also lets us detect
 * protocol problems quickly.
 */
#define MAX_MESSAGE_SIZE (32*1024*1024)

static unsigned char *read_message (void);
static void write_reply (ETERM *);

int
main (void)
{
  unsigned char *buf;
  ETERM *ret, *message;

  erl_init (NULL, 0);

  /* This process has a single libguestfs handle.  If the Erlang
   * system creates more than one handle, then more than one of these
   * processes will be running.
   */
  g = guestfs_create ();
  if (g == NULL)
    error (EXIT_FAILURE, 0, "could not create guestfs handle");

  guestfs_set_error_handler (g, NULL, NULL);

  while ((buf = read_message ()) != NULL) {
    message = erl_decode (buf);
    free (buf);

    ret = dispatch (message);
    erl_free_term (message);

    write_reply (ret);
    erl_free_term (ret);
  }

  guestfs_close (g);

  exit (EXIT_SUCCESS);
}

/* The Erlang port always sends the length of the buffer as 4
 * bytes in network byte order, followed by the message buffer.
 */
static unsigned char *
read_message (void)
{
  uint32_t buf;
  size_t size;
  unsigned char *r;

  errno = 0;
  if (full_read (0, &buf, 4) != 4) {
    if (errno == 0) /* ok - closed connection normally */
      return NULL;
    else
      error (EXIT_FAILURE, errno, "read message size");
  }

  size = ntohl (buf);

  if (size > MAX_MESSAGE_SIZE)
    error (EXIT_FAILURE, 0, "message larger than MAX_MESSAGE_SIZE");

  r = malloc (size);
  if (r == NULL)
    error (EXIT_FAILURE, errno, "malloc");

  if (full_read (0, r, size) != size)
    error (EXIT_FAILURE, errno, "read message content");

  return r;
}

static void
write_reply (ETERM *term)
{
  size_t size;
  unsigned char sbuf[4];
  unsigned char *buf;

  size = erl_term_len (term);

  buf = malloc (size);
  if (buf == NULL)
    error (EXIT_FAILURE, errno, "malloc");

  erl_encode (term, buf);

  sbuf[0] = (size >> 24) & 0xff;
  sbuf[1] = (size >> 16) & 0xff;
  sbuf[2] = (size >> 8) & 0xff;
  sbuf[3] = size & 0xff;

  if (full_write (1, sbuf, 4) != 4)
    error (EXIT_FAILURE, errno, "write message size");

  if (full_write (1, buf, size) != size)
    error (EXIT_FAILURE, errno, "write message content");

  free (buf);
}

/* Note that all published Erlang code/examples etc uses strncmp in
 * a buggy way.  This is the right way to do it.
 */
int
atom_equals (ETERM *atom, const char *name)
{
  const size_t namelen = strlen (name);
  const size_t atomlen = ERL_ATOM_SIZE (atom);
  if (namelen != atomlen) return 0;
  return strncmp (ERL_ATOM_PTR (atom), name, atomlen) == 0;
}

ETERM *
make_error (const char *funname)
{
  ETERM *error = erl_mk_atom ("error");
  ETERM *msg = erl_mk_string (guestfs_last_error (g));
  ETERM *num = erl_mk_int (guestfs_last_errno (g));
  ETERM *t[3] = { error, msg, num };
  return erl_mk_tuple (t, 3);
}

ETERM *
unknown_function (ETERM *fun)
{
  ETERM *unknown = erl_mk_atom ("unknown");
  ETERM *funcopy = erl_copy_term (fun);
  ETERM *t[2] = { unknown, funcopy };
  return erl_mk_tuple (t, 2);
}

ETERM *
unknown_optarg (const char *funname, ETERM *optargname)
{
  ETERM *unknownarg = erl_mk_atom ("unknownarg");
  ETERM *copy = erl_copy_term (optargname);
  ETERM *t[2] = { unknownarg, copy };
  return erl_mk_tuple (t, 2);
}

ETERM *
make_string_list (char **r)
{
  size_t i, size;
  CLEANUP_FREE ETERM **t = NULL;

  for (size = 0; r[size] != NULL; ++size)
    ;

  t = malloc (sizeof (ETERM *) * size);
  if (t == NULL)
    return make_error ("make_string_list");

  for (i = 0; r[i] != NULL; ++i)
    t[i] = erl_mk_string (r[i]);

  return erl_mk_list (t, size);
}

/* Make a hash table.  The number of elements returned by the C
 * function is always even.
 */
ETERM *
make_table (char **r)
{
  size_t i, size;
  CLEANUP_FREE ETERM **t = NULL;
  ETERM *a[2];

  for (size = 0; r[size] != NULL; ++size)
    ;

  t = malloc (sizeof (ETERM *) * (size/2));
  if (t == NULL)
    return make_error ("make_table");

  for (i = 0; r[i] != NULL; i += 2) {
    a[0] = erl_mk_string (r[i]);
    a[1] = erl_mk_string (r[i+1]);
    t[i/2] = erl_mk_tuple (a, 2);
  }

  return erl_mk_list (t, size/2);
}

ETERM *
make_bool (int r)
{
  if (r)
    return erl_mk_atom ("true");
  else
    return erl_mk_atom ("false");
}

char **
get_string_list (ETERM *term)
{
  ETERM *t;
  size_t i, size;
  char **r;

  for (size = 0, t = term; !ERL_IS_EMPTY_LIST (t);
       size++, t = ERL_CONS_TAIL (t))
    ;

  r = malloc ((size+1) * sizeof (char *));
  if (r == NULL)
    error (EXIT_FAILURE, errno, "malloc");

  for (i = 0, t = term; !ERL_IS_EMPTY_LIST (t); i++, t = ERL_CONS_TAIL (t))
    r[i] = erl_iolist_to_string (ERL_CONS_HEAD (t));
  r[size] = NULL;

  return r;
}

int
get_bool (ETERM *term)
{
  if (atom_equals (term, "true"))
    return 1;
  else
    return 0;
}

int
get_int (ETERM *term)
{
  switch (ERL_TYPE (term)) {
  case ERL_INTEGER:
    return ERL_INT_VALUE (term);
  case ERL_U_INTEGER:
    return (int) ERL_INT_UVALUE (term);
  case ERL_LONGLONG:
    /* XXX check for overflow */
    return (int) ERL_LL_VALUE (term);
  case ERL_U_LONGLONG:
    /* XXX check for overflow */
    return (int) ERL_LL_UVALUE (term);
  default:
    /* XXX fail in some way */
    return -1;
  }
}

int64_t
get_int64 (ETERM *term)
{
  switch (ERL_TYPE (term)) {
  case ERL_INTEGER:
    return ERL_INT_VALUE (term);
  case ERL_U_INTEGER:
    return ERL_INT_UVALUE (term);
  case ERL_LONGLONG:
    return ERL_LL_VALUE (term);
  case ERL_U_LONGLONG:
    return (int64_t) ERL_LL_UVALUE (term);
  default:
    /* XXX fail in some way */
    return -1;
  }
}
