// ---------------------------------------------------------------------------
// - Socket.cpp                                                              -
// - afnix:net module - socket class implementation                          -
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - 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.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2011 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Item.hpp"
#include "Regex.hpp"
#include "Vector.hpp"
#include "Socket.hpp"
#include "Integer.hpp"
#include "Boolean.hpp"
#include "Runnable.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"
#include "cnet.hpp"
#include "csio.hpp"
#include "cerr.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // the socket eval quarks
  static const long QUARK_REUSEADDR = String::intern ("REUSE-ADDRESS");
  static const long QUARK_BROADCAST = String::intern ("BROADCAST");
  static const long QUARK_DONTROUTE = String::intern ("DONT-ROUTE");
  static const long QUARK_KEEPALIVE = String::intern ("KEEP-ALIVE");
  static const long QUARK_LINGER    = String::intern ("LINGER");
  static const long QUARK_RCVSIZE   = String::intern ("RCV-SIZE");
  static const long QUARK_SNDSIZE   = String::intern ("SND-SIZE");
  static const long QUARK_HOPLIMIT  = String::intern ("HOP-LIMIT");
  static const long QUARK_MCASTLOOP = String::intern ("MULTICAST-LOOPBACK");
  static const long QUARK_MCASTHOP  = String::intern ("MULTICAST-HOP-LIMIT");
  static const long QUARK_MAXSEG    = String::intern ("MAX-SEGMENT-SIZE");
  static const long QUARK_NODELAY   = String::intern ("NO-DELAY");
  static const long QUARK_SOCKET    = String::intern ("Socket");

  // map an enumeration item to a socket option
  static inline Socket::t_option item_to_option (const Item& item) {
    // check for a socket item
    if (item.gettid () != QUARK_SOCKET)
      throw Exception ("item-error", "item is not an socket item");
    // map the item to the enumeration
    long quark = item.getquark ();
    if (quark == QUARK_REUSEADDR) return Socket::SOCK_REUSEADDR;
    if (quark == QUARK_BROADCAST) return Socket::SOCK_BROADCAST;
    if (quark == QUARK_DONTROUTE) return Socket::SOCK_DONTROUTE;
    if (quark == QUARK_KEEPALIVE) return Socket::SOCK_KEEPALIVE;
    if (quark == QUARK_LINGER)    return Socket::SOCK_LINGER;
    if (quark == QUARK_RCVSIZE)   return Socket::SOCK_RCVSIZE;
    if (quark == QUARK_SNDSIZE)   return Socket::SOCK_SNDSIZE;
    if (quark == QUARK_HOPLIMIT)  return Socket::SOCK_HOPLIMIT;
    if (quark == QUARK_MCASTLOOP) return Socket::SOCK_MCASTLOOP;
    if (quark == QUARK_MCASTHOP)  return Socket::SOCK_MCASTHOP;
    if (quark == QUARK_MAXSEG)    return Socket::SOCK_MAXSEG;
    if (quark == QUARK_NODELAY)   return Socket::SOCK_NODELAY;
    throw Exception ("item-error", "cannot map item to socket option");
  }

  // generate an authority by address and port
  static String to_authority (const Address* addr, const long port) {
    // initialize result
    String result = "0.0.0.0:0";
    if (addr == nilp) return result;
    // check port value
    if (port < 0) {
      throw Exception ("socket-error", "invalid negative port number");
    }
    // get canonical address
    String cnam = addr->getcanon ();
    // check for IPV6
    Regex re = "<$x:>+";
    if (re == cnam) {
      result  = '[';
      result += cnam;
      result += ']';
    } else {
      result = cnam;
    }
    // add the port
    result += ':';
    result += port;
    // here it is
    return result;
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------

  // create a default socket

  Socket::Socket (void) {
    d_sid = -1;
  }

  // create a socket by id

  Socket::Socket (const int sid) {
    d_sid = sid;
  }

  // destroy this socket

  Socket::~Socket (void) {
    close ();
  }

  // return the class name

  String Socket::repr (void) const {
    return "Socket";
  }

  // return the stream descriptor

  int Socket::getsid (void) const {
    rdlock ();
    try {
      int result = d_sid;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if we have an ipv6 socket

  bool Socket::isipv6 (void) const {
    rdlock ();
    try {
      bool result = c_isipv6 (d_sid);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if we can broadcast message

  bool Socket::isbcast (void) const {
    rdlock ();
    try {
      bool result = false;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return true if the socket is open

  bool Socket::isopen (void) const {
    rdlock ();
    try {
      bool status = (d_sid != -1);
      unlock ();
      return status;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // close this socket 
  
  bool Socket::close (void) {
    wrlock ();
    try {
      if ((d_sid == -1) || (Object::uref (this) == false)) {
	unlock ();
	return true;
      }
      if (c_close (d_sid) == false) {
	unlock ();
	return false;
      }
      d_sid = -1;
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // force a socket shutdown
  
  bool Socket::shutdown (void) {
    wrlock ();
    try {
      if (d_sid == -1) {
	unlock ();
	return true;
      }
      if (c_close (d_sid) == false) {
	unlock ();
	return false;
      }
      d_sid = -1;
      unlock ();
      return true;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // shutdown this socket

  bool Socket::shutdown (const bool mode) {
    wrlock ();
    try {
      bool result = false;
      if (mode == false) result = c_ipshut (d_sid, SOCKET_SHUT_RECV);
      if (mode == true)  result = c_ipshut (d_sid, SOCKET_SHUT_SEND);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a socket option

  bool Socket::setopt (t_option opt, bool flag) {
    wrlock ();
    try {
      bool result = false;
      switch (opt) {
      case SOCK_REUSEADDR:
	result = c_ipsetopt (d_sid, SOCKET_REUSEADDR, flag, 0);
	break;    
      case SOCK_BROADCAST:
	result = c_ipsetopt (d_sid, SOCKET_BROADCAST, flag, 0);
	break;
      case SOCK_DONTROUTE:
	result = c_ipsetopt (d_sid, SOCKET_DONTROUTE, flag, 0);
	break;
      case SOCK_KEEPALIVE:
	result = c_ipsetopt (d_sid, SOCKET_KEEPALIVE, flag, 0);
	break;
      case SOCK_MCASTLOOP:
	result = c_ipsetopt (d_sid, SOCKET_MCASTLOOP, flag, 0);
	break;
      case SOCK_NODELAY:
	result = c_ipsetopt (d_sid, SOCKET_NODELAY,   flag, 0);
	break;
      default:
	break;
      }
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a socket option with a value

  bool Socket::setopt (t_option opt, bool flag, long val) {
    wrlock ();
    try {
      bool result = false;
      switch (opt) {
      case SOCK_LINGER:
	result = c_ipsetopt (d_sid, SOCKET_LINGER,   flag, val);
	break;
      case SOCK_RCVSIZE:
	result = c_ipsetopt (d_sid, SOCKET_RCVSIZE,  flag, val);
	break;
      case SOCK_SNDSIZE:
	result = c_ipsetopt (d_sid, SOCKET_SNDSIZE,  flag, val);
	break;
      case SOCK_HOPLIMIT:
	result = c_ipsetopt (d_sid, SOCKET_HOPLIMIT, flag, val);
	break;
      case SOCK_MCASTHOP:
	result = c_ipsetopt (d_sid, SOCKET_MCASTHOP, flag, val);
	break;
      case SOCK_MAXSEG:
	result = c_ipsetopt (d_sid, SOCKET_MAXSEG,   flag, val);
	break;
      default:
	break;
      }
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // connect this socket by port and address

  bool Socket::connect (t_word port, const Address& addr) {
    rdlock ();
    try {
      bool result = c_ipconnect (d_sid, port, addr.p_addr);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // connect this socket by port and aliases address

  bool Socket::connect (t_word port, const Address& addr, const bool alsf) {
    wrlock ();
    addr.rdlock ();
    try {
      // start with canonical address
      bool status = c_ipconnect (d_sid, port, addr.p_addr);
      if ((status == true) || (alsf == false)) {
	addr.unlock ();
	unlock ();
	return status;
      }
      // use aliases address mode
      for (long i = 0; i < addr.d_size; i++) {
	status = c_ipconnect (d_sid, port, addr.p_aals[i].p_aadr);
	if (status == true) {
	  addr.unlock ();
	  unlock ();
	  return true;
	}
      }
      // connection failure
      addr.unlock ();
      unlock ();
      return false;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // bind a socket with a port

  bool Socket::bind (t_word port) {
    rdlock ();
    try {
      bool result = c_ipbind (d_sid, port);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // bind a socket with a port and an address

  bool Socket::bind (t_word port, const Address& addr) {
    rdlock ();
    try {
      addr.rdlock ();
      bool result = c_ipbind (d_sid, port, addr.p_addr);
      addr.unlock ();
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the socket address

  Address* Socket::getsockaddr (void) const {
    rdlock ();
    t_byte* addr = nilp;
    try {
      addr = c_ipsockaddr (d_sid);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
    try {
      Address* result = (addr == nilp) ? nilp : new Address (addr);
      delete [] addr;
      return result;
    } catch (...) {
      delete [] addr;
      throw;
    }
  }

  // return the socket port

  t_word Socket::getsockport (void) const {
    rdlock ();
    try {
      t_word result = c_ipsockport (d_sid);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the socket authority

  String Socket::getsockauth (void) const {
    rdlock ();
    Address* addr = nilp;
    try {
      // get the socket address
      addr = getsockaddr ();
      // get the socket port
      long port = getsockport ();
      // map it to an authority
      String result = to_authority (addr, port);
      delete addr;
      // unlock and return
      unlock ();
      return result;
    } catch (...) {
      delete addr;
      unlock ();
      throw;
    }
  }

  // return the peer address

  Address* Socket::getpeeraddr (void) const {
    rdlock ();
    t_byte* addr = nilp;
    try {
      addr = c_ippeeraddr (d_sid);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
    try {    
      Address* result = (addr == nilp) ? nilp : new Address (addr);
      delete [] addr;
      return result;
    } catch (...) {
      delete [] addr;
      throw;
    }
  }

  // return the peer port

  t_word Socket::getpeerport (void) const {
    rdlock ();
    try {
      t_word result = c_ippeerport (d_sid);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the peer authority

  String Socket::getpeerauth (void) const {
    rdlock ();
    Address* addr = nilp;
    try {
      // get the peer address
      addr = getpeeraddr ();
      // get the peer port
      long port = getpeerport ();
      // map it to an authority
      String result = to_authority (addr, port);
      delete addr;
      // unlock and return
      unlock ();
      return result;
    } catch (...) {
      delete addr;
      unlock ();
      throw;
    }
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 18;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);
  
  // the object supported quarks
  static const long QUARK_BIND      = zone.intern ("bind");
  static const long QUARK_CLOSE     = zone.intern ("close");
  static const long QUARK_OPENP     = zone.intern ("open-p");
  static const long QUARK_IPV6P     = zone.intern ("ipv6-p");
  static const long QUARK_BCASTP    = zone.intern ("broadcast-p");
  static const long QUARK_SETOPT    = zone.intern ("set-option");
  static const long QUARK_CONNECT   = zone.intern ("connect");
  static const long QUARK_SETEMOD   = zone.intern ("set-encoding-mode");
  static const long QUARK_SHUTDOWN  = zone.intern ("shutdown");
  static const long QUARK_SOCKADDR  = zone.intern ("get-socket-address");
  static const long QUARK_SOCKPORT  = zone.intern ("get-socket-port");
  static const long QUARK_SOCKAUTH  = zone.intern ("get-socket-authority");
  static const long QUARK_PEERADDR  = zone.intern ("get-peer-address");
  static const long QUARK_PEERPORT  = zone.intern ("get-peer-port");
  static const long QUARK_PEERAUTH  = zone.intern ("get-peer-authority");
  static const long QUARK_SETOPTION = zone.intern ("set-option");
  static const long QUARK_SETIEMOD  = zone.intern ("set-input-encoding-mode");
  static const long QUARK_GETIEMOD  = zone.intern ("get-input-encoding-mode");
  static const long QUARK_SETOEMOD  = zone.intern ("set-output-encoding-mode");
  static const long QUARK_GETOEMOD  = zone.intern ("get-output-encoding-mode");

  // local quarks
  static const long QUARK_GETEMOD   = String::intern ("get-encoding-mode");

  // return true if the given quark is defined

  bool Socket::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? InputStream::isquark (quark, hflg) : false;
    if (result == false) {
      result = hflg ? OutputStream::isquark (quark, hflg) : false;
    }
    unlock ();
    return result;
  }

  // evaluate a quark statically

  Object* Socket::meval (Runnable* robj, Nameset* nset, const long quark) {
    if (quark == QUARK_REUSEADDR) 
      return new Item (QUARK_SOCKET, QUARK_REUSEADDR);
    if (quark == QUARK_BROADCAST) 
      return new Item (QUARK_SOCKET, QUARK_BROADCAST);
    if (quark == QUARK_DONTROUTE) 
      return new Item (QUARK_SOCKET, QUARK_DONTROUTE);
    if (quark == QUARK_MCASTLOOP) 
      return new Item (QUARK_SOCKET, QUARK_MCASTLOOP);
    if (quark == QUARK_KEEPALIVE) 
      return new Item (QUARK_SOCKET, QUARK_KEEPALIVE);
    if (quark == QUARK_LINGER)    
      return new Item (QUARK_SOCKET, QUARK_LINGER);
    if (quark == QUARK_RCVSIZE)   
      return new Item (QUARK_SOCKET, QUARK_RCVSIZE);
    if (quark == QUARK_SNDSIZE)   
      return new Item (QUARK_SOCKET, QUARK_SNDSIZE);
    if (quark == QUARK_HOPLIMIT)  
      return new Item (QUARK_SOCKET, QUARK_HOPLIMIT);
    if (quark == QUARK_MCASTHOP)  
      return new Item (QUARK_SOCKET, QUARK_MCASTHOP);
    if (quark == QUARK_MAXSEG) 
      return new Item (QUARK_SOCKET, QUARK_MAXSEG);
    if (quark == QUARK_NODELAY) 
      return new Item (QUARK_SOCKET, QUARK_NODELAY);
    throw Exception ("eval-error", "cannot evaluate member",
		     String::qmap (quark));
  }

  // apply this object with a set of arguments and a quark

  Object* Socket::apply (Runnable* robj, Nameset* nset, const long quark,
			 Vector* argv) {

    // check the special quark first
    if (quark == QUARK_SETIEMOD) {
      return InputStream::apply (robj, nset, QUARK_SETEMOD, argv);
    }
    if (quark == QUARK_GETIEMOD) {
      return InputStream::apply (robj, nset, QUARK_GETEMOD, argv);
    }
    if (quark == QUARK_SETOEMOD) {
      return OutputStream::apply (robj, nset, QUARK_SETEMOD, argv);
    }    
    if (quark == QUARK_GETOEMOD) {
      return OutputStream::apply (robj, nset, QUARK_GETEMOD, argv);
    }
    if (quark == QUARK_SETEMOD) {
      Object::cref (InputStream::apply  (robj, nset, quark, argv));
      Object::cref (OutputStream::apply (robj, nset, quark, argv));
      return nilp;
    }
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();

    // dispatch 0 argument
    if (argc == 0) {
      if (quark == QUARK_IPV6P)    return new Boolean (isipv6 ());
      if (quark == QUARK_OPENP)    return new Boolean (isopen ());
      if (quark == QUARK_BCASTP)   return new Boolean (isbcast ());
      if (quark == QUARK_SOCKPORT) return new Integer (getsockport ());
      if (quark == QUARK_PEERPORT) return new Integer (getpeerport ());
      if (quark == QUARK_SOCKAUTH) return new String  (getsockauth ());
      if (quark == QUARK_PEERAUTH) return new String  (getpeerauth ());
      if (quark == QUARK_CLOSE)    return new Boolean (close       ());
      if (quark == QUARK_SHUTDOWN) return new Boolean (shutdown    ());
      if (quark == QUARK_SOCKADDR) {
	rdlock ();
	try {
	  Object* result = getsockaddr ();
	  robj->post (result);
	  unlock ();
	  return result;
	} catch (...) {
	  unlock ();
	  throw;
	}
      }
      if (quark == QUARK_PEERADDR) {
	rdlock ();
	try {
	  Object* result = getpeeraddr ();
	  robj->post (result);
	  unlock ();
	  return result;
	} catch (...) {
	  unlock ();
	  throw;
	}
      }
    }

    // dispatch 1 argument
    if (argc == 1) {
      if (quark == QUARK_BIND) {
	long port = argv->getlong (0);
	bind (port);
	return nilp;
      }
      if (quark == QUARK_SHUTDOWN) {
	bool mode = argv->getbool (0);
	return new Boolean (shutdown (mode));
      }
    }
    // dispatch 2 arguments
    if (argc == 2) {
      if (quark == QUARK_BIND) {
	long port = argv->getlong (0);
	Address* addr = dynamic_cast <Address*> (argv->get (1));
	if (addr == nilp) 
	  throw Exception ("argument-error", "address expected with bind");
	bind (port, *addr);
	return nilp;
      }
      if (quark == QUARK_CONNECT) {
	long port = argv->getlong (0);
	Address* addr = dynamic_cast <Address*> (argv->get (1));
	if (addr == nilp) 
	  throw Exception ("argument-error", "address expected with connect");
	connect (port, *addr);
	return nilp;
      }
      if (quark == QUARK_SETOPTION) {
	Item* item = dynamic_cast <Item*> (argv->get (0));
	if (item == nilp) throw Exception ("argument-error",
					   "invalid object as socket option");
	t_option opt = item_to_option (*item);
	Object* obj = argv->get (1);
	Boolean* bobj = dynamic_cast <Boolean*> (obj);
	if (bobj != nilp) {
	  bool flg = bobj->tobool ();
	  return new Boolean (setopt (opt, flg));
	}
	Integer* iobj = dynamic_cast <Integer*> (obj);
	if (iobj != nilp) {
	  long val = iobj->tolong (); 
	  return new Boolean (setopt (opt, true, val));
	}
	throw Exception ("argument-error", "invalid argument with set-option");
      }
    }
    // dispatch 3 arguments
    if (argc == 3) {
      if (quark == QUARK_SETOPTION) {
	Item* item = dynamic_cast <Item*> (argv->get (0));
	if (item == nilp) throw Exception ("argument-error",
					   "invalid object as socket option");
	t_option opt = item_to_option (*item);
	bool flg = argv->getbool (1);
	long val = argv->getlong (2);
	return new Boolean (setopt (opt, flg, val));
      }
      if (quark == QUARK_CONNECT) {
	long port = argv->getlong (0);
	Address* addr = dynamic_cast <Address*> (argv->get (1));
	if (addr == nilp) 
	  throw Exception ("argument-error", "address expected with connect");
	bool alsf = argv->getbool (2);
	connect (port, *addr, alsf);
	return nilp;
      }
    }
    // check for input stream quark
    if (InputStream::isquark (quark, true) == true)
      return InputStream::apply (robj, nset, quark, argv);
    // check for output stream quark
    if (OutputStream::isquark (quark, true) == true)
      return OutputStream::apply (robj, nset, quark, argv);
    // call the object method
    return Object::apply (robj, nset, quark, argv);
  }
}
