#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <ruby.h>

/* #define HAVE_IPV6 */
/* #define HAVE_SOCKADDR_LEN */
/* #define HAVE_AF_LINK */

static VALUE mSockAddr,
             cSockAddr,
#ifdef HAVE_AF_LINK
             cSockAddrDl,
#endif /* HAVE_AF_LINK */
             cSockAddrIn,
#ifdef HAVE_IPV6
             cSockAddrIn6,
#endif /* HAVE_IPV6 */
             cSockAddrUn;

/*******************************/
/* SockAddr::SockAddr methods */
/*******************************/
VALUE sockaddr_new(int argc, VALUE *argv, VALUE klass) {
  VALUE self;
  struct sockaddr *sa;

  sa = malloc(sizeof(struct sockaddr));
  memset(sa, 0, sizeof(struct sockaddr));

  self = Data_Wrap_Struct(klass, 0, free, sa);
  rb_obj_call_init(self, argc, argv);

  return self;
}

static VALUE addr_init(int argc, VALUE *argv, VALUE self) {
  struct sockaddr *sa;

  Data_Get_Struct(self, struct sockaddr, sa);
  switch (argc) {
    case 0:
      /* zero args: don't do anything to the struct */
      break;

    case 1:
      if (TYPE(argv[0]) == T_STRING) {
        /* sanity check on initializer string */
        if (RSTRING(argv[0])->len < sizeof(struct sockaddr))
          rb_raise(rb_eArgError, "initializer value too small "
                                  "(<= sizeof(struct sockaddr))");

        memcpy(sa, RSTRING(argv[0])->ptr, sizeof(struct sockaddr));
      } else {
        rb_raise(rb_eArgError, "invalid initializer value (not a String)");
      }
    default:
      rb_raise(rb_eArgError, "invalid argument count (not 0)");
  }
  
  return self;
}

#ifdef HAVE_SOCKADDR_LEN
static VALUE addr_len(VALUE self) {
  VALUE len;
  struct sockaddr *sa;
  /* use this to force type promotion on the length from a signed byte
   * to an integer so we can use INT2FIX */
  int t_len;

  Data_Get_Struct(self, struct sockaddr, sa);
  t_len = sa->sa_len;
  
  return INT2FIX(t_len);
}

static VALUE addr_set_len(VALUE self, VALUE len) {
  struct sockaddr *sa;
  int newlen;

  Data_Get_Struct(self, struct sockaddr, sa);
  newlen = NUM2INT(len);

  if (newlen < sizeof(struct sockaddr) || newlen > 255)
    rb_warning("truncating invalid length %d", newlen);

  sa->sa_len = (uint8_t) newlen;
  
  return len;
}
#endif /* HAVE_SOCKADDR_LEN */

static VALUE addr_family(VALUE self) {
  struct sockaddr *sa;
  int t_fam;

  Data_Get_Struct(self, struct sockaddr, sa);
  t_fam = sa->sa_family;
  
  switch (t_fam) {
    case AF_INET:
    case AF_INET6:
    case AF_LOCAL:
#ifdef HAVE_AF_LINK
    case AF_LINK:
#endif /* HAVE_AF_LINK */
      /* don't print a warning */
      break;
    default:
      /* FIXME: maybe throw exception instead? */
      rb_warning("invalid or unknown family type %d", t_fam);
  }

  return NUM2INT(t_fam);
}

static VALUE addr_set_family(VALUE self, VALUE family) {
  struct sockaddr *sa;
  int newfam;

  Data_Get_Struct(self, struct sockaddr, sa);
  newfam = NUM2INT(family);

  /* warn if we don't know the family type, but let them set it anyway;
   * maybe they know what they're doing ;) */
  if (newfam != AF_INET  && 
      newfam != AF_INET6 &&
      newfam != AF_LOCAL
#ifdef HAVE_AF_LINK
      && newfam != AF_LINK
#endif /* HAVE_AF_LINK */
  )
    rb_warning("SockAddr::SockAddr#len: non-standard family %d "
               "(!= AF_INET, AF_INET6, AF_LOCAL, or AF_LINK)", 
               newfam);

  sa->sa_family = newfam;
  
  return family;
}

static VALUE addr_data(VALUE self) {
  struct sockaddr *sa;

  Data_Get_Struct(self, struct sockaddr, sa);

  return rb_str_new(sa->sa_data, 14);
}

static VALUE addr_set_data(VALUE self, VALUE data) {
  struct sockaddr *sa;

  Data_Get_Struct(self, struct sockaddr, sa);
  if (RSTRING(data)->len > 14)
    rb_warning("SockAddr::SockAddr#data=: "
               "truncating data length to 14 characters.");

  memset(sa->sa_data, 0, 14);
  memcpy(sa->sa_data, RSTRING(data)->ptr, RSTRING(data)->len);
  
  return data;
}

static VALUE addr_to_s(VALUE self) {
  struct sockaddr *sa;
  Data_Get_Struct(self, struct sockaddr, sa);

  return rb_str_new((char *) sa, sizeof(struct sockaddr));
}

/********************************/
/* SockAddr::SockAddrIn methods */
/********************************/
VALUE sockaddr_in_new(int argc, VALUE *argv, VALUE klass) {
  VALUE self;
  struct sockaddr_in *sa;

  sa = malloc(sizeof(struct sockaddr_in));
  memset(sa, 0, sizeof(struct sockaddr_in));

  self = Data_Wrap_Struct(klass, 0, free, sa);
  rb_obj_call_init(self, argc, argv);

  return self;
}

static VALUE addr_in_port(VALUE self) {
  struct sockaddr_in *sa;
  /* used for free type promotion */
  int port;
  
  Data_Get_Struct(self, struct sockaddr_in, sa);
  port = sa->sin_port;
  
  return INT2FIX(port);
}

static VALUE addr_in_set_port(VALUE self, VALUE port) {
  struct sockaddr_in *sa;
  int newport;
  
  Data_Get_Struct(self, struct sockaddr_in, sa);
  newport = NUM2INT(port);

  if (newport <= 0 || newport > 65535)
    rb_raise(rb_eRangeError, "invalid port %d (port <= 0 or port > 65535)",
             newport);

  sa->sin_port = (in_port_t) newport;
  
  return port;
}

static VALUE addr_in_addr(VALUE self) {
  struct sockaddr_in *sa;
  Data_Get_Struct(self, struct sockaddr_in, sa);
  return rb_str_new((char *) &(sa->sin_addr), sizeof(struct in_addr));
}

static VALUE addr_in_set_addr(VALUE self, VALUE new_addr) {
  struct sockaddr_in *sa;

  Data_Get_Struct(self, struct sockaddr_in, sa);
  if (RSTRING(new_addr)->len != sizeof(struct in_addr))
    rb_raise(rb_eArgError, "invalid address string length %d",
             RSTRING(new_addr)->len);

  /* memset(&(sa->sin_addr), 0, sizeof(struct in_addr)); */
  memcpy(&(sa->sin_addr), RSTRING(new_addr)->ptr, sizeof(struct in_addr));

  return new_addr;
}

static VALUE addr_in_zero(VALUE self) {
  struct sockaddr_in *sa;
  Data_Get_Struct(self, struct sockaddr_in, sa);
  return rb_str_new(sa->sin_zero, 8);
}

static VALUE addr_in_set_zero(VALUE self, VALUE zero) {
  struct sockaddr_in *sa;

  Data_Get_Struct(self, struct sockaddr_in, sa);

  if (RSTRING(zero)->len != 8)
    rb_raise(rb_eArgError, "invalid string length %d", RSTRING(zero)->len);

  memcpy(sa->sin_zero, RSTRING(zero)->ptr, 8);
  
  return rb_str_new(sa->sin_zero, 8);
}

static VALUE addr_in_to_s(VALUE self) {
  struct sockaddr_in *sa;
  Data_Get_Struct(self, struct sockaddr_in, sa);

  return rb_str_new((char *) sa, sizeof(struct sockaddr_in));
}

/********************************/
/* SockAddr::SockAddrUn methods */
/********************************/
VALUE sockaddr_un_new(int argc, VALUE *argv, VALUE klass) {
  VALUE self;
  struct sockaddr_un *sa;

  sa = malloc(sizeof(struct sockaddr_in));
  memset(sa, 0, sizeof(struct sockaddr_in));

  self = Data_Wrap_Struct(klass, 0, free, sa);
  rb_obj_call_init(self, argc, argv);

  return self;
}

static VALUE addr_un_path(VALUE self) {
  struct sockaddr_un *sa;
  Data_Get_Struct(self, struct sockaddr_un, sa);
  return rb_str_new2(sa->sun_path);
}

static VALUE addr_un_set_path(VALUE self, VALUE path) {
  struct sockaddr_un *sa;
  
  Data_Get_Struct(self, struct sockaddr_un, sa);

  if (RSTRING(path)->len >= sizeof(sa->sun_path))
    rb_raise(rb_eRangeError, "invalid path length %d", RSTRING(path)->len);

  strncpy(sa->sun_path, RSTRING(path)->ptr, RSTRING(path)->len);

  return path;
}

void Init_sockaddr(void) {
  mSockAddr = rb_define_module_under(rb_cSocket, "SockAddr");

  /*************************/
  /* define SockAddr class */
  /*************************/
  cSockAddr = rb_define_class_under(mSockAddr, "SockAddr", rb_cObject);
  rb_define_singleton_method(cSockAddr, "new", sockaddr_new, -1);
  rb_define_method(cSockAddr, "initialize", addr_init, -1);
#ifdef HAVE_SOCKADDR_LEN
  rb_define_method(cSockAddr, "len", addr_len, 0);
  rb_define_method(cSockAddr, "len=", addr_set_len, 1);
#endif /* HAVE_SOCKADDR_LEN */
  rb_define_method(cSockAddr, "family", addr_family, 0);
  rb_define_method(cSockAddr, "family=", addr_set_family, 1);
  rb_define_method(cSockAddr, "data", addr_data, 0);
  rb_define_method(cSockAddr, "data=", addr_set_data, 1); /* 14 chars */
  rb_define_method(cSockAddr, "to_s", addr_to_s, 0);

  /***************************/
  /* define SockAddrIn class */
  /***************************/
  cSockAddrIn = rb_define_class_under(mSockAddr, "SockAddrIn", cSockAddr);
  rb_define_singleton_method(cSockAddrIn, "new", sockaddr_in_new, -1);
  rb_define_method(cSockAddrIn, "initialize", addr_init, -1);
  rb_define_method(cSockAddrIn, "port", addr_in_port, 0);
  rb_define_method(cSockAddrIn, "port=", addr_in_set_port, 1);
  rb_define_method(cSockAddrIn, "addr", addr_in_addr, 0);
  rb_define_method(cSockAddrIn, "addr=", addr_in_set_addr, 1);
  rb_define_method(cSockAddrIn, "address", addr_in_addr, 0);
  rb_define_method(cSockAddrIn, "address=", addr_in_set_addr, -1);
  rb_define_method(cSockAddrIn, "zero", addr_in_zero, 0);
  rb_define_method(cSockAddrIn, "zero=", addr_in_set_zero, 1); /* 8 chars */
  rb_define_method(cSockAddrIn, "to_s", addr_in_to_s, 0);

#if HAVE_IPV6
  /****************************/
  /* define SockAddrIn6 class */
  /****************************/
  cSockAddrIn6 = rb_define_class_under(mSockAddr, "SockAddrIn6", cSockAddr);
  rb_define_singleton_method(cSockAddrIn6, "new", sockaddr_in6_new, -1);
  rb_define_method(cSockAddrIn6, "initialize", addr_init, -1);
  rb_define_method(cSockAddrIn6, "port", addr_in6_port, 0);
  rb_define_method(cSockAddrIn6, "port=", addr_in6_set_port, 1);
  rb_define_method(cSockAddrIn6, "addr", addr_in6_addr, 0);
  rb_define_method(cSockAddrIn6, "addr=", addr_in6_set_addr, 1);
  rb_define_method(cSockAddrIn6, "address", addr_in6_addr, 0);
  rb_define_method(cSockAddrIn6, "address=", addr_in6_set_addr, -1);
  rb_define_method(cSockAddrIn6, "flowinto", addr_in6_flowinto, 0);
  rb_define_method(cSockAddrIn6, "flowinto=", addr_in6_set_flowinto, 1);
  /* rb_define_method(cSockAddrIn6, "to_s", addr_in6_to_s, 0); */
#endif /* HAVE_IPV6 */
  
  /***************************/
  /* define SockAddrUn class */
  /***************************/
  cSockAddrUn = rb_define_class_under(mSockAddr, "SockAddrUn", cSockAddr);
  rb_define_singleton_method(cSockAddrUn, "new", sockaddr_un_new, -1);
  rb_define_method(cSockAddrUn, "initialize", addr_init, -1);
  rb_define_method(cSockAddrUn, "path", addr_un_path, 0);
  rb_define_method(cSockAddrUn, "path=", addr_un_set_path, 1);
  /* rb_define_method(cSockAddrUn, "to_s", un_to_s, 0); */
}

