/*
 * Copyright (C) 2001-2004 Peter J Jones (pjones@pmade.org)
 * All Rights Reserved
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name of the Author nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/** @file
 * This file contains the implementation of the Netxx::Datagram class.
**/

// common header
#include "common.h"

// Netxx includes
#include "datagram.h"
#include "address.h"
#include "peer.h"
#include "socket.h"
#include "sockaddr.h"
#include "recvfrom.h"

// standard includes
#include <string>
#include <memory>
#include <cstring>
#include <vector>

//####################################################################
namespace 
{
    void call_connect(Netxx::Socket &socket, const Netxx::Peer &peer, std::vector<std::string> &files);
    void call_bind(Netxx::Socket &socket, std::vector<std::string> &files);
    void get_tmp_filename (std::string &tmp_name);
}
//####################################################################
struct Netxx::Datagram::pimpl 
{
    pimpl (const Timeout &timeout) : timeout_(timeout) {}
    pimpl (const Timeout &timeout, socket_type socketfd) : timeout_(timeout), socket_(socketfd) {}

    ~pimpl (void) 
    {
#   ifndef WIN32
	if (!files_.empty()) {
	    socket_.close();

	    std::vector<std::string>::const_iterator i(files_.begin()), end(files_.end());
	    for (; i!=end; ++i) unlink(i->c_str());
	}
#   endif
    }

    Timeout timeout_;
    Socket socket_;
    ProbeInfo pi_;
    std::vector<std::string> files_;
};
//####################################################################
Netxx::Datagram::Datagram (const Address &connect_to, const Timeout &timeout) 
{
    if (!connect_to.size()) throw Exception("no addresses to call connect for");
    std::auto_ptr<pimpl> ap(pimpl_ = new pimpl(timeout));

    call_connect(pimpl_->socket_, *connect_to.begin(), pimpl_->files_);
    pimpl_->pi_.add_socket(pimpl_->socket_.get_socketfd());

    ap.release();
}
//####################################################################
Netxx::Datagram::Datagram (const char *peer_name, port_type default_port, const Timeout &timeout) 
{
    Address connect_to(peer_name, default_port);
    std::auto_ptr<pimpl> ap(pimpl_ = new pimpl(timeout));

    call_connect(pimpl_->socket_, *connect_to.begin(), pimpl_->files_);
    pimpl_->pi_.add_socket(pimpl_->socket_.get_socketfd());

    ap.release();
}
//####################################################################
Netxx::Datagram::Datagram (socket_type socketfd, const Timeout &timeout) 
{
    std::auto_ptr<pimpl> ap (pimpl_ = new pimpl(timeout, socketfd));

    pimpl_->pi_.add_socket(pimpl_->socket_.get_socketfd());

    ap.release();
}
//####################################################################
Netxx::Datagram::Datagram (const Timeout &timeout) 
{
    std::auto_ptr<pimpl> ap (pimpl_ = new pimpl(timeout));

    pimpl_->pi_.add_socket(pimpl_->socket_.get_socketfd());

    ap.release();
}
//####################################################################
Netxx::Datagram::~Datagram (void) 
{
    delete pimpl_;
}
//####################################################################
Netxx::signed_size_type Netxx::Datagram::send (const Peer &peer, const void *buffer, size_type length) 
{
#   if defined(WIN32)
	const char *buffer_ptr = static_cast<const char*>(buffer);
#   else
	const void *buffer_ptr = buffer;
#   endif

    const sockaddr *sa = reinterpret_cast<const sockaddr*>(peer.get_sa());
    size_type sa_size = peer.get_sa_size();
    bool correct_type = false;

    /*
     * make sure our socket is setup to handle this type of peer
     */
    if (peer.get_socketfd() != -1 && peer.get_socketfd() == pimpl_->socket_.get_socketfd()) {
	// we have to be ready!
	correct_type = true;
    } else if (pimpl_->socket_.get_socketfd() >= 0) {
	Socket::Type stype = pimpl_->socket_.get_type();

	switch (stype) {
#   ifndef WIN32
	    case Socket::LOCALDGRAM:
		correct_type = sa->sa_family == AF_LOCAL;
		break;
#   endif

	    case Socket::UDP:
		correct_type = sa->sa_family == AF_INET;
		break;

#   ifndef NETXX_NO_INET6
	    case Socket::UDP6:
		correct_type = sa->sa_family == AF_INET6;
		break;
#   endif

	    default:
		break;
	}
    }

    if (!correct_type) {
	Socket::Type stype;

	switch (sa->sa_family) {
	    case AF_INET:
		stype = Socket::UDP;
		break;

#   ifndef NETXX_NO_INET6
	    case AF_INET6:
		stype = Socket::UDP6;
		break;
#   endif

#   ifndef WIN32
	    case AF_LOCAL:
		stype = Socket::LOCALDGRAM;
		break;
#   endif
	    default:
		stype = Socket::UDP;
	}

	Socket tmp_socket(stype);
	pimpl_->socket_.swap(tmp_socket);
	call_bind(pimpl_->socket_, pimpl_->files_);
    }

    if (pimpl_->timeout_ && !pimpl_->socket_.writable(pimpl_->timeout_))
	return -1;

    signed_size_type rc;
    if ( (rc = sendto(pimpl_->socket_.get_socketfd(), buffer_ptr, length, 0, sa, sa_size)) < 0) {
	std::string error("sendto(2) failure: ");
	error += str_error(get_last_error());
	throw Exception(error);
    }

    return rc;
}
//####################################################################
Netxx::Datagram::receive_type Netxx::Datagram::receive (void *buffer, size_type length) 
{
    /*
     * if this is not a connected datagram, the socket might not be ready
     * yet. We can't receive a datagram because we don't know if it should
     * be a AF_INET, AF_INET6 or even a AF_LOCAL datagram.
     */
    if (pimpl_->socket_.get_socketfd() < 0)
	throw Exception("can't receive datagram unless one is sent first");

    if (pimpl_->timeout_ && !pimpl_->socket_.readable(pimpl_->timeout_))
	return std::make_pair(-1, Peer());

    return call_recvfrom(pimpl_->socket_, buffer, length);
}
//####################################################################
Netxx::signed_size_type Netxx::Datagram::write (const void *buffer, size_type length) 
{
    return pimpl_->socket_.write(buffer, length, pimpl_->timeout_);
}
//####################################################################
Netxx::signed_size_type Netxx::Datagram::read (void *buffer, size_type length) 
{
    return pimpl_->socket_.read(buffer, length, pimpl_->timeout_);
}
//####################################################################
void Netxx::Datagram::close (void) 
{
    pimpl_->socket_.close();
    pimpl_->pi_.clear();
}
//####################################################################
void Netxx::Datagram::release (void) 
{
    pimpl_->socket_.release();
}
//####################################################################
const Netxx::ProbeInfo* Netxx::Datagram::get_probe_info (void) const 
{
    return &(pimpl_->pi_);
}
//####################################################################
namespace 
{
    //####################################################################
    void call_connect (Netxx::Socket &socket, const Netxx::Peer &peer, std::vector<std::string> &files) 
    {
	const sockaddr *sa = reinterpret_cast<const sockaddr*>(peer.get_sa());
	Netxx::size_type sa_size = peer.get_sa_size();
	Netxx::Socket::Type stype;

	switch (sa->sa_family) {
	    case AF_INET:
		stype = Netxx::Socket::UDP;
		break;


# ifndef NETXX_NO_INET6
	    case AF_INET6:
		stype = Netxx::Socket::UDP6;
		break;
# endif

# ifndef WIN32
	    case AF_LOCAL:
		stype = Netxx::Socket::LOCALDGRAM;
		break;
# endif

	    default:
		stype = Netxx::Socket::UDP;
	}

	Netxx::Socket tmp_socket(stype);
	socket.swap(tmp_socket);
	call_bind(socket, files);

	if (connect(socket.get_socketfd(), sa, sa_size) != 0) {
	    std::string error("connect(2) failed: ");
	    error += Netxx::str_error(Netxx::get_last_error());
	    throw Netxx::NetworkException(error);
	}
    }
    //####################################################################
    void call_bind (Netxx::Socket &socket, std::vector<std::string> &files) 
    {
#	ifndef WIN32
	    if (socket.get_type() == Netxx::Socket::LOCALDGRAM) {
		Netxx::SockAddr socket_addr(socket.get_type());
		sockaddr_un *saun = reinterpret_cast<sockaddr_un*>(socket_addr.get_sa());
		Netxx::size_type saun_size = socket_addr.get_sa_size();

		std::string tmp_name;
		get_tmp_filename(tmp_name);
		files.push_back(tmp_name);

		if (tmp_name.size() >= sizeof(saun->sun_path))
		    throw Netxx::Exception("temp name is too large for sockaddr_un");

		std::strcpy(saun->sun_path, tmp_name.c_str());
		
		if (bind(socket.get_socketfd(), reinterpret_cast<sockaddr*>(saun), saun_size) != 0) {
		    std::string error("bind(2) error: ");
		    error += Netxx::str_error(Netxx::get_last_error());
		    throw Netxx::NetworkException(error);
		}

		/*
		 * This will make a local domain socket more like a real
		 * socket since anyone with local file system access can
		 * connect to it. This may be a bad thing in this case
		 * though. More to come.
		 */
		chmod(saun->sun_path, 0666);
	    }
#	endif
    }
    //####################################################################
    void get_tmp_filename (std::string &tmp_name) 
    {
#	ifndef WIN32
	    char buffer[] = "/tmp/Netxx_Domain_Socket.XXXXXXXXXXXXXXXXX";
	    int fd;

	    if ( (fd = mkstemp(buffer)) < 0) {
		std::string error("can't create temporary file: ");
		error += Netxx::str_error(Netxx::get_last_error());
		throw Netxx::NetworkException(error);
	    }

	    /*
	     * WARNING WARNING WARNING WARNING
	     *
	     * The following action could create a race condition.  It may
	     * not be that big of a deal in this case since when we call
	     * bind(2) and it will fail if the file in sun_path already
	     * exists. 
	     *
	     * So, if we close and unlink the file, and then before we call
	     * bind(2) someone creates a file with the same name, bind
	     * should fail.
	     *
	     * It might just be better to use tmpnam(3) but then you have to
	     * see the linker warning about tmpnam might be used in an
	     * unsafe manner. I think what we have here should be good
	     * enough.
	     *
	     * WARNING WARNING WARNING WARNING
	     */
	    close(fd);
	    unlink(buffer);

	    tmp_name = buffer;
#	endif
    }
    //####################################################################
} // end anonymous namespace
