/* BTP library - Banana Tree Protocol
 * Copyright (C) 1999-2001  The Regents of the University of Michigan
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the 
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 */


#include <sys/types.h>
#include <sys/time.h>

#include "b_conn_ping.h"
#include "b_packet.h"
#include "util.h"
#include "btp_debug.h"


static void 	receive_any  (BConn* conn, BPacket* packet, gpointer user_data);
static void 	receive_ping (BConn* conn, BPacket* packet, gpointer user_data);
static void 	receive_pong (BConn* conn, BPacket* packet, gpointer user_data);

static void	send_ping (BConn* conn);

static gboolean	send_ping_cb (gpointer data);
static gboolean	conn_timeout_cb (gpointer data);



/* **************************************** */


/**

   Send a ping to a host.  If we already know the distance to the
   host, we set a timer to send the ping in the future.  Otherwise, we
   send the ping now.  We will continue pinging the host until reset
   is called.

 */
void
b_conn_ping_start (BConn* conn)
{
  g_return_if_fail (conn);
  g_return_if_fail (b_conn_is_connected (conn));
  g_return_if_fail (!b_conn_is_closing (conn));

  BTPP (1, "b_conn_ping_start\n");

  if (conn->is_pinging) return;
  conn->is_pinging = TRUE;

  /* Set conn timeout */
  rand_timer_set (&conn->conn_timeout_timer, BTP_PING_TIMEOUT, 
		  0, conn_timeout_cb, conn);

  /* Start handling any packet */
  b_conn_handler_add (conn, -1, -1, receive_any, NULL);

  /* Start handling PONGs */
  b_conn_handler_add (conn, B_PACKET_TYPE_INFO_REPLY, B_PACKET_INFO_SUBTYPE_PING,
		      receive_pong, NULL);

  /* If we don't know the distance, send a PING immediately. */
  if (conn->distance == 0)
    {
      send_ping (conn);
    }
  /* Otherwise, schedule one soon */
  else
    {
      /* Start sending PINGs */
      rand_timer_set (&conn->send_ping_timer, BTP_PING_SEND_TIME, 
		      BTP_PING_SEND_RANGE, send_ping_cb, conn);
    }
}



/**

   Reset the ping timers.  This will stop us from pinging the host.

 */
void
b_conn_ping_stop (BConn* conn)
{
  g_return_if_fail (conn);

  BTPP (1, "b_conn_ping_stop\n");

  if (!conn->is_pinging) return;
  conn->is_pinging = FALSE;

  /* Stop sending PINGs */
  rand_timer_cancel (&conn->send_ping_timer);

  /* Don't timeout the connection */
  rand_timer_cancel (&conn->conn_timeout_timer);

  /* Stop handline PONGs */
  b_conn_handler_remove (conn, B_PACKET_TYPE_INFO_REPLY, 
			 B_PACKET_INFO_SUBTYPE_PING, NULL);

  /* Stop handling any packet */
  b_conn_handler_remove (conn, -1, -1, NULL);

  /* Remove all pings from the queue */
  b_conn_remove_packets_by_subtype (conn, B_PACKET_TYPE_INFO_REQUEST, 
				    B_PACKET_INFO_SUBTYPE_PING);
}


void
b_conn_ping_handle (BConn* conn)
{
  g_return_if_fail (conn);
  g_return_if_fail (b_conn_is_connected (conn));
  g_return_if_fail (!b_conn_is_closing (conn));

  /* Start handling PINGs */
  b_conn_handler_add (conn, B_PACKET_TYPE_INFO_REQUEST, B_PACKET_INFO_SUBTYPE_PING,
		      receive_ping, NULL);
}


void
b_conn_ping_unhandle (BConn* conn)
{
  g_return_if_fail (conn);

  /* Stop handling PINGs */
  b_conn_handler_remove (conn, B_PACKET_TYPE_INFO_REQUEST, 
			 B_PACKET_INFO_SUBTYPE_PING, NULL);
}





/* **************************************** */



/**

   Called when a packet has been received for the host.  This resets
   the connection timeout timer.

 */
static void
receive_any (BConn* conn, BPacket* packet, gpointer user_data)
{
  BTPP (1, "receive_any\n");

  g_return_if_fail (conn->is_pinging);

  /* Reset conn timeout */
  rand_timer_reset (&conn->conn_timeout_timer, BTP_PING_TIMEOUT, 
		    0, conn_timeout_cb, conn);
}



/**

   Process a PING packet received from this host.  We immediately send
   back a PONG.

   This callback is set once we have connect.

 */
static void
receive_ping (BConn* conn, BPacket* packet, gpointer user_data)
{
  BPacket* new_packet;

  g_return_if_fail (conn);
  g_return_if_fail (packet);
  g_return_if_fail (packet->type == B_PACKET_TYPE_INFO_REQUEST);
  g_return_if_fail (packet->subtype == B_PACKET_INFO_SUBTYPE_PING);

  BTPP (1, "receive_ping\n");

  /* Make sure the ping isn't too big */
  if (g_ntohs(packet->length) > 64)
    {
      g_warning ("Received very long PING - ignoring\n");
      return;
    }

  /* Send a pong back */
  new_packet = b_packet_new_info_reply_ping (conn, packet);
  b_conn_send_packet (conn, new_packet);
}



/**

   Process a PONG packet received from this host.  We calculate the
   distance to the host.

   TODO: Check if pong was sent.  Worst case, we get messed up
   distance estimate.

 */
static void
receive_pong (BConn* conn, BPacket* packet, gpointer user_data)
{
  struct timeval* time_sent;
  struct timeval timeofday;
  guint distance;

  g_return_if_fail (conn);
  g_return_if_fail (packet);
  g_return_if_fail (packet->type == B_PACKET_TYPE_INFO_REPLY);
  g_return_if_fail (packet->subtype == B_PACKET_INFO_SUBTYPE_PING);

  BTPP (1, "receive_pong\n");

  /* Make sure payload is the correct length */
  if (g_ntohs(packet->length) != sizeof(struct timeval))
    {
      g_warning ("Received weird pong\n");
      b_conn_func_fail (conn);
      return;
    }

  time_sent = (struct timeval*) packet->data;
  gettimeofday (&timeofday, NULL);

  /* Make sure the pong isn't from the future */
  if (timercmp (time_sent, &timeofday, >))
    {
      g_warning ("Received pong from future.\n");
      b_conn_func_fail (conn);
      return;
    }

  /* Get the distance (in ms) */
  timersub (&timeofday, time_sent, &timeofday);
  distance = timeofday.tv_sec * 1000 + timeofday.tv_usec / 1000;
  distance = MAX(distance, 1);

  /* Save the distance. If we have an old estimate, take the weighted
     average. */
  if (conn->distance)
    {
      conn->distance = (((double) conn->distance) * (BTP_PING_AVG_FACTOR)) +
	(((double) distance) * (1.0 - BTP_PING_AVG_FACTOR));
      if (conn->distance <= 0)
	conn->distance = 1;
    }
  else
    {
      conn->distance = distance;
    }

  BTPP (5, "PING: %s:%d is %d (%d)\n", 
	conn->conn->hostname, conn->conn->port, 
	conn->distance, distance);

  b_conn_func_ping (conn);
}




/* **************************************** */

void
b_conn_ping_send (BConn* conn)
{
  /* Clear ping timer */
  rand_timer_cancel (&conn->send_ping_timer);

  /* Send ping now.  The timer will be reset in send_ping() */
  send_ping (conn);
}


static void
send_ping (BConn* conn)
{
  g_return_if_fail (conn != NULL);

  BTPP (1, "send_ping\n");

  /* Send a ping only if there isn't one already in the queue.  */
  if (!b_conn_has_packet_by_subtype(conn, B_PACKET_TYPE_INFO_REQUEST, 
				    B_PACKET_INFO_SUBTYPE_PING))
    {
      BPacket* packet;

      /* Send a ping to the host */
      packet = b_packet_new_info_request_ping (conn);
      b_conn_send_packet (conn, packet);
    }

  /* Reset timer */
  rand_timer_set (&conn->send_ping_timer, BTP_PING_SEND_TIME, 
		  BTP_PING_SEND_RANGE, send_ping_cb, conn);
}



/* **************************************** */

static gboolean
send_ping_cb (gpointer data)
{
  BConn* conn = (BConn*) data;

  BTPP (1, "send_ping_cb\n");

  g_return_val_if_fail (conn, FALSE);
  g_return_val_if_fail (conn->send_ping_timer, FALSE);

  conn->send_ping_timer = 0;

  send_ping (conn);

  return FALSE;
}



static gboolean
conn_timeout_cb (gpointer data)
{
  BConn* conn = (BConn*) data;

  BTPP (1, "conn_timeout_cb\n");

  g_return_val_if_fail (conn, FALSE);
  g_return_val_if_fail (conn->conn_timeout_timer, FALSE);

  conn->conn_timeout_timer = 0;

  b_conn_func_fail (conn);

  return FALSE;
}


syntax highlighted by Code2HTML, v. 0.9.1