/* udpclient.c
 * copyright © 2010-2015 IOhannes m zmölnig, IEM
 */

/*                                                                              */
/* A client for bidirectional communication from within Pd.                     */
/*                                                                              */
/* 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, see                                         */
/*     http://www.gnu.org/licenses/                                             */
/*                                                                              */

/* ---------------------------------------------------------------------------- */

#define DEBUGLEVEL 1

#include "iemnet.h"
#include <string.h>

#include <pthread.h>

static t_class *udpclient_class;
static const char objName[] = "udpclient";

typedef struct _udpclient {
  t_object x_obj;
  t_outlet*x_msgout;
  t_outlet*x_addrout;
  t_outlet*x_connectout;
  t_outlet*x_statusout;

  t_iemnet_sender*x_sender;
  t_iemnet_receiver*x_receiver;

  int x_fd; /* the socket */
  const char*x_hostname; /* address we want to connect to as text */
  int x_connectstate; /* 0 = not connected, 1 = connected */
  u_short x_port; /* port we're sending to */
  u_short x_sendport; /* port we're sending from */

  long x_addr; /* address we're connected to as 32bit int */

  t_iemnet_floatlist*x_floatlist;
} t_udpclient;


/* forward declarations */
static void udpclient_receive_callback(void *x, t_iemnet_chunk*);



static void udpclient_info(t_udpclient *x)
{
  /*
    "server <socket> <IP> <port>"
    "bufsize <insize> <outsize>"
  */
  static t_atom output_atom[3];
  int connected = x->x_connectstate;
  int sockfd = x->x_fd;
  if(sockfd >= 0)
    iemnet__socket2addressout(sockfd, x->x_statusout, gensym("local_address"));
  iemnet__numconnout(x->x_statusout, x->x_connectout, x->x_connectstate);
  if(connected) {
    unsigned short port = x->x_port;
    const char*hostname = x->x_hostname;

    int insize = iemnet__receiver_getsize(x->x_receiver);
    int outsize = iemnet__sender_getsize(x->x_sender);

    SETFLOAT(output_atom+0, sockfd);
    SETSYMBOL(output_atom+1, gensym(hostname));
    SETFLOAT(output_atom+2, port);
    outlet_anything(x->x_statusout, gensym("server"), 3, output_atom);

    SETFLOAT(output_atom+0, insize);
    SETFLOAT(output_atom+1, outsize);
    outlet_anything(x->x_statusout, gensym("bufsize"), 2, output_atom);
  }
}

/* connection handling */
static void *udpclient_doconnect(t_udpclient*x, int subthread)
{
  struct sockaddr_in server;
  struct hostent*hp;
  int sockfd;
  int broadcast = 1;/* nonzero is true */
  memset(&server, 0, sizeof(server));

  if (x->x_sender) {
    iemnet_log(x, IEMNET_ERROR, "already connected");
    return (x);
  }

  /* connect socket using hostname provided in command line */
  hp = gethostbyname(x->x_hostname);
  if (hp == 0) {
    iemnet_log(x, IEMNET_ERROR, "bad host '%s'?", x->x_hostname);
    return (x);
  }
  server.sin_family = AF_INET;

  /* create a socket */
  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  DEBUG("send socket %d\n", sockfd);
  if (sockfd < 0) {
    iemnet_log(x, IEMNET_ERROR, "unable to create socket");
    sys_sockerror("socket");
    return (x);
  }

  /* Enable sending of broadcast messages (if hostname is a broadcast address) */
#ifdef SO_BROADCAST
  if( 0 != setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST,
                      (const void *)&broadcast, sizeof(broadcast))) {
    iemnet_log(x, IEMNET_ERROR, "unable to switch to broadcast mode");
    sys_sockerror("setsockopt");
  }
#endif /* SO_BROADCAST */

  if(x->x_sendport>0) {
    server.sin_family = AF_INET;
    server.sin_port = htons(x->x_sendport);
    server.sin_addr.s_addr = INADDR_ANY;
    if (bind(sockfd, (struct sockaddr *) &server, sizeof (server)) < 0) {
      iemnet_log(x, IEMNET_ERROR, "unable to bind with sending port %d (continuing with random port)", x->x_sendport);
      sys_sockerror("bind");
    }
  }

  /* try to connect. */
  /* assign client port number */
  memcpy((char *)&server.sin_addr, (char *)hp->h_addr, hp->h_length);
  server.sin_port = htons(x->x_port);
  DEBUG("connecting to %s:%d", x->x_hostname, x->x_port);

  if (connect(sockfd, (struct sockaddr *) &server, sizeof (server)) < 0) {
    iemnet_log(x, IEMNET_ERROR, "unable to connect to stream socket");
    sys_sockerror("connect");
    iemnet__closesocket(sockfd, 1);
    return (x);
  }

  x->x_fd = sockfd;
  x->x_addr = ntohl(*(long *)hp->h_addr);

  x->x_sender = iemnet__sender_create(sockfd, NULL, NULL, subthread);
  x->x_receiver = iemnet__receiver_create(sockfd, x,
                                        udpclient_receive_callback, subthread);

  x->x_connectstate = 1;
  udpclient_info(x);
  return (x);
}

static int udpclient_do_disconnect(t_udpclient *x)
{
  DEBUG("disconnect %x %x", x->x_sender, x->x_receiver);
  if(x->x_receiver) {
    iemnet__receiver_destroy(x->x_receiver, 0);
  }
  x->x_receiver = NULL;
  if(x->x_sender) {
    iemnet__sender_destroy(x->x_sender, 0);
  }
  x->x_sender = NULL;

  x->x_connectstate = 0;
  if (x->x_fd < 0) {
    return 0;
  }
  iemnet__closesocket(x->x_fd, 1);
  x->x_fd = -1;
  return 1;
}
static void udpclient_disconnect(t_udpclient *x) {
  if(!udpclient_do_disconnect(x)) {
    iemnet_log(x, IEMNET_ERROR, "not connected");
  } else {
    iemnet__numconnout(x->x_statusout, x->x_connectout, x->x_connectstate);
  }
}

static void udpclient_connect(t_udpclient *x, t_symbol *hostname,
                              t_floatarg fportno,
			      t_floatarg fsndportno)
{
  if(x->x_fd >= 0) {
    udpclient_disconnect(x);
  }
  /* we get hostname and port and pass them on
     to the child thread that establishes the connection */
  x->x_hostname = hostname->s_name;
  x->x_port = fportno;
  x->x_sendport = (fsndportno>0)?fsndportno:0;
  x->x_connectstate = 0;
  udpclient_doconnect(x, 0);
}

/* sending/receiving */
static void udpclient_send(t_udpclient *x, t_symbol *s, int argc,
                           t_atom *argv)
{
  int size = 0;
  t_atom output_atom;
  t_iemnet_sender*sender = x->x_sender;
  t_iemnet_chunk*chunk = iemnet__chunk_create_list(argc, argv);
  (void)s; /* ignore unused variable */

  if(sender && chunk) {
    size = iemnet__sender_send(sender, chunk);
  }
  iemnet__chunk_destroy(chunk);

  SETFLOAT(&output_atom, size);
  outlet_anything( x->x_statusout, gensym("sendbuffersize"), 1,
                   &output_atom);
}

static void udpclient_receive_callback(void*y, t_iemnet_chunk*c)
{
  t_udpclient *x = (t_udpclient*)y;

  if(c) {
    iemnet__addrout(x->x_statusout, x->x_addrout, x->x_addr, x->x_port);
    x->x_floatlist = iemnet__chunk2list(c,
                                      x->x_floatlist); /* gets destroyed in the dtor */
    outlet_list(x->x_msgout, gensym("list"),x->x_floatlist->argc,
                x->x_floatlist->argv);
  } else {
    /* disconnected */
    DEBUG("disconnected");
    if(x->x_fd >= 0) {
      udpclient_disconnect(x);
    }
  }
}

/* constructor/destructor */

static void *udpclient_new(void)
{
  t_udpclient *x = (t_udpclient *)pd_new(udpclient_class);
  x->x_msgout = outlet_new(&x->x_obj, 0); /* received data */
  x->x_addrout = outlet_new(&x->x_obj, gensym("list"));
  x->x_connectout = outlet_new(&x->x_obj,
                               gensym("float")); /* connection state */
  x->x_statusout = outlet_new(&x->x_obj,
                              0); /* last outlet for everything else */

  x->x_fd = -1;
  x->x_addr = 0L;
  x->x_port = 0;

  x->x_sender = NULL;
  x->x_receiver = NULL;

  x->x_floatlist = iemnet__floatlist_create(1024);

  return (x);
}

static void udpclient_free(t_udpclient *x)
{
  udpclient_do_disconnect(x);
  if(x->x_floatlist) {
    iemnet__floatlist_destroy(x->x_floatlist);
  }
  x->x_floatlist = NULL;
}

IEMNET_EXTERN void udpclient_setup(void)
{
  if(!iemnet__register(objName)) {
    return;
  }
  udpclient_class = class_new(gensym(objName), (t_newmethod)udpclient_new,
                              (t_method)udpclient_free,
                              sizeof(t_udpclient), 0, A_DEFFLOAT, 0);
  class_addmethod(udpclient_class, (t_method)udpclient_connect,
                  gensym("connect"), A_SYMBOL, A_FLOAT, A_DEFFLOAT, 0);
  class_addmethod(udpclient_class, (t_method)udpclient_disconnect,
                  gensym("disconnect"), 0);
  class_addmethod(udpclient_class, (t_method)udpclient_send, gensym("send"),
                  A_GIMME, 0);
  class_addlist(udpclient_class, (t_method)udpclient_send);
  class_addbang(udpclient_class, (t_method)udpclient_info);

  DEBUGMETHOD(udpclient_class);
}


IEMNET_INITIALIZER(udpclient_setup);

/* end of udpclient.c */
