#ifndef SOCKET_H_
#define SOCKET_H_

#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <exception>
#include <errno.h>

#include <sys/types.h>       // per data types
#include <sys/socket.h>      // per socket(), connect(), send(), e recv()
#include <netdb.h>           // per gethostbyname()
#include <arpa/inet.h>       // per inet_addr()
#include <unistd.h>          // per close()
#include <netinet/in.h>      // per sockaddr_in
using namespace std;

typedef void raw_type; // tipo di base utilizzato nel seguito


/**
 *   Segnala un problema nell'esecuzione di una socket call.
 */
class SocketException: public exception {

private:
	string userMessage;

public:

	/**
	 *  Costruisce una SocketException con un messaggio informativo.
	 *  @param message è il messaggio
	 *  @param incSysMsg true se si tratta di un messaggio di sistema (strerror(errno))
	 */
	SocketException(const string &message, bool inclSysMsg) throw () :
		userMessage(message) {
		if (inclSysMsg) {
			userMessage.append(": ");
			userMessage.append(strerror(errno));
		}
	}

	~SocketException() throw () {
	}

	/**
	 * Restituisce il messaggio di eccezione
	 */
	const char *what() const throw () {
		return userMessage.c_str();
	}

};

/**
 *   Astrazione per l'input stream associato ad un socket.
 */
class InputStream {

private:
	int sockDesc; //identificativo o descrittore del socket

public:
	InputStream(int sd) {
		sockDesc = sd;
	}

	virtual ~InputStream() {
	}

	/**
	 * Legge e restituisce un carattere ricevuto sullo stream di input di un socket.
	 * Puo' essere usato solo in presenza di una connessione.
	 */
	char read() {
		int rtn;

		//il risultato ottenuto e' contenuto nella memoria puntata da "input"
		char * input = new char[1];

		if ((rtn = ::recv(sockDesc, (raw_type *) input, 1, 0)) < 0) {
			throw SocketException("Received failed (recv())", true);
			//Il risultato della chiamata a questa funzione, in caso di errore, e' "-1",
			//altrimenti e' il numero di caratteri ricevuti
		}
		return input[0];
	}

	/**
	 * Legge e restituisce una linea ricevuta sullo stream di input di un socket.
	 * Puo' essere usato solo in presenza di una connessione.
	 */
	string readLine() {
		int rtn;
		string str = "";

		//il risultato ottenuto e' contenuto nella memoria puntata da "input"
		char * input = new char[1];

		//costruisce il risultato un carattere per volta fino a che non
		//trova un carattere di '\n'
		do {
			if ((rtn = ::recv(sockDesc, (raw_type *) input, 1, 0)) < 0) {
				throw SocketException("Received failed (recv())", true);
				//Il risultato della chiamata a questa funzione, in caso di errore, e' "-1",
				//altrimenti e' il numero di caratteri ricevuti
			}
			str += input[0];
		} while (input[0] != '\n');

		return str;
	}
};

/**
 *   Astrazione per l'output stream associato ad un socket.
 */
class OutputStream {

private:
	int sockDesc; //identificativo o descrittore del socket

	char *convertStringToChar(const string &str) {

		char *retPtr(new char[str.length() + 1]);
		copy(str.begin(), str.end(), retPtr);
		retPtr[str.length()] = '\0';
		return retPtr;
	}
public:

	OutputStream(int sd) {
		sockDesc = sd;
	}

	virtual ~OutputStream() {
	}

	/**
	 * Scrive un carattere sullo stream di output associato ad un socket.
	 * Puo' essere usato solo in presenza di una connessione.
	 */
	void write(char c) {

		char *output = new char[1];
		output[0] = c;

		//Questa funzione invia messaggi dal socket rappresentato dal descrittore
		//"sockDesc" al socket con cui e' connesso.
		if (::send(sockDesc, (raw_type *) output, 1, 0) < 0) {
			throw SocketException("Send failed (send())", true);
			//In caso di errore, la funzione "send" restituisce il valore "-1",
			//altrimenti restituisce "0".
		}
	}

	/**
	 * Scrive una stringa sullo stream di output associato ad un socket.
	 * Puo' essere usato solo in presenza di una connessione.
	 */
	void writeBytes(string str) {

		char *output(convertStringToChar(str));

		//Questa funzione invia messaggi dal socket rappresentato dal descrittore
		//"sockDesc" al socket con cui e' connesso.
		if (::send(sockDesc, (raw_type *) output, str.length(), 0) < 0) {
			throw SocketException("Send failed (send())", true);
			//In caso di errore, la funzione "send" restituisce il valore "-1",
			//altrimenti restituisce "0".
		}
	}

};

class Socket {

private:

	InputStream *is;
	OutputStream *os;

	void connect(const string &foreignAddress, unsigned short foreignPort)
			throw (SocketException) {

		sockaddr_in destAddr;
		fillAddr(foreignAddress, foreignPort, destAddr);

		//Cerca di effettuare la connessione fra il socket passato come parametro
		//con il socket in ascolto all'indirizzo specificato.
		if (::connect(sockDesc, (sockaddr *) &destAddr, sizeof(destAddr)) < 0) {
			throw SocketException("Connect failed (connect())", true);
		}
	}

	static void fillAddr(const string &address, unsigned short port,
			sockaddr_in &addr) {
		memset(&addr, 0, sizeof(addr)); // Zero out address structure
		addr.sin_family = AF_INET; // Internet address

		hostent *host; // Resolve name
		if ((host = gethostbyname(address.c_str())) == NULL) {
			throw SocketException("Failed to resolve name (gethostbyname())",
					true);
		}
		addr.sin_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
		addr.sin_port = htons(port); // Assign port in network byte order
	}

protected:
	int sockDesc; // Socket descriptor

	void setLocalAddressAndPort(const string &localAddress,
			unsigned short localPort) throw (SocketException) {

		sockaddr_in localAddr;
		fillAddr(localAddress, localPort, localAddr);

		if (bind(sockDesc, (sockaddr *) &localAddr, sizeof(sockaddr_in)) < 0) {
			throw SocketException(
					"Set of local address and port failed (bind())", true);
		}
	}

	void setLocalPort(unsigned short localPort) throw (SocketException) {

		sockaddr_in localAddr;
		memset(&localAddr, 0, sizeof(localAddr));
		localAddr.sin_family = AF_INET;
		localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
		localAddr.sin_port = htons(localPort);

		if (bind(sockDesc, (sockaddr *) &localAddr, sizeof(sockaddr_in)) < 0) {
			throw SocketException("Set of local port failed (bind())", true);
		}
	}
public:

	Socket() throw (SocketException) {
		if ((sockDesc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
			throw SocketException("Socket creation failed (socket())", true);
		}
		is = new InputStream(sockDesc);
		os = new OutputStream(sockDesc);
	}

	Socket(const string &foreignAddress, unsigned short foreignPort)
			throw (SocketException) {

		if ((sockDesc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
			throw SocketException("Socket creation failed (socket())", true);
		}
		is = new InputStream(sockDesc);
		os = new OutputStream(sockDesc);

		connect(foreignAddress, foreignPort);
	}

	Socket(int type, int protocol) throw (SocketException) {

		if ((sockDesc = socket(PF_INET, type, protocol)) < 0) {
			throw SocketException("Socket creation failed (socket())", true);
		}
		is = new InputStream(sockDesc);
		os = new OutputStream(sockDesc);
	}

	Socket(int sockDesc) {
		this->sockDesc = sockDesc;

		is = new InputStream(sockDesc);
		os = new OutputStream(sockDesc);
	}

	/**
	 *   Chiude e dealloca il socket
	 */

	~Socket() {
		::close(sockDesc);
		sockDesc = -1;
		delete is;
		delete os;
	}

	/**
	 * Restituisce l'OutputStream in cui scrivere i messaggi da inviare tramite il socket.
	 *
	 */
	OutputStream* getOutputStream() {
		return os;
	}

	/**
	 * Restituisce l' InputStream da cui leggere i messaggi provenienti dal socket.
	 *
	 */
	InputStream* getInputStream() {
		return is;
	}

	/**
	 * Consente di chiudere la comunicazione sul socket identificato dal
	 * descrittore "sockDesc".
	 */
	void close() {
		::close(sockDesc);
		sockDesc = -1;
	}

	/**
	 *   Restituisce l'indirizzo locale a cui il socket è associato
	 *   @return local address
	 *   @exception SocketException se fallisce il recupero dell'indirizzo
	 */
	string getLocalAddress() throw (SocketException) {
		sockaddr_in addr;
		unsigned int addr_len = sizeof(addr);


		if (getsockname(sockDesc, (sockaddr *) &addr, (socklen_t *) &addr_len)
				< 0) {
			throw SocketException(
					"Fetch of local address failed (getsockname())", true);
		}
		return inet_ntoa(addr.sin_addr);
	}

	/**
	 *   Restituisce la porta locale a cui il socket è associato
	 *   @return local port
	 *   @exception SocketException se fallisce il recupero dell'indirizzo
	 */
	unsigned short getLocalPort() throw (SocketException) {
		sockaddr_in addr;
		unsigned int addr_len = sizeof(addr);

		//Permette di ottenere tramite "addr" le informazioni sull'indirizzo locale del socket
		if (getsockname(sockDesc, (sockaddr *) &addr, (socklen_t *) &addr_len)
				< 0) {
			throw SocketException("Fetch of local port failed (getsockname())",
					true);
		}
		return ntohs(addr.sin_port);
	}

};

class ServerSocket: public Socket {

private:
	void setListen(int queueLen) throw (SocketException) {
		//Mentre il socket e' in ascolto, puo' ricevere delle richieste di connessione.
		//Mentre viene servita una di queste richieste, ne possono arrivare altre.
		//Il procedimento adottato per tenere conto di questo fatto, e' di mettere le richieste in una coda di attesa.
		//Listen si occupa di definire la dimensione massima di questa coda ("dimensione_coda").
		if (listen(sockDesc, queueLen) < 0) {
			throw SocketException("Set listening socket failed (listen())",
					true);
		}
	}
public:
	ServerSocket(unsigned short localPort, int queueLen)
			throw (SocketException) :
		Socket(SOCK_STREAM, IPPROTO_TCP) {
		setLocalPort(localPort);
		setListen(queueLen);
	}

	ServerSocket(const string &localAddress, unsigned short localPort,
			int queueLen) throw (SocketException) :
		Socket(SOCK_STREAM, IPPROTO_TCP) {
		setLocalAddressAndPort(localAddress, localPort);
		setListen(queueLen);
	}

	ServerSocket(unsigned short localPort) throw (SocketException) :
		Socket(SOCK_STREAM, IPPROTO_TCP) {
		int queueLen = 5;
		setLocalPort(localPort);
		setListen(queueLen);
	}

	/**
	 * Ha il compito di accettare una connessione, prendendo la prima connessione
	 * disponibile sulla coda delle connessioni pendenti (vedi listen),
	 * crea un nuovo socket con le stesse proprieta' di quello rappresentato dal
	 * descrittore "sockDesc" e restituisce un nuovo descrittore.
	 * La connessione puo', allora, essere gestita con questo nuovo socket.
	 */
	Socket *accept() throw (SocketException) {
		int newConnSD;
		if ((newConnSD = ::accept(sockDesc, NULL, 0)) < 0) {
			throw SocketException("Accept failed (accept())", true);
		}

		return new Socket(newConnSD);
	}
};

#endif /* SOCKET_H_ */
