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

#include "jmchat.h"
#include "jmchat_debug.h"

#include "util/elf.h"
#include "util/util.h"

static void	chat_send_join (JMChat* chat);
static void 	chat_send_leave (JMChat* chat);
static void	chat_reset (JMChat* chat);
static JMChatMember* chat_get_member_update (JMChat* chat, const gchar* nick);

JMChatMember*	member_new (const gchar* nick);
static void	member_delete (JMChatMember* member);

static void     set_update_timer (JMChat* chat);
static void     send_update (JMChat* chat);
static gboolean send_update_cb (gpointer data);
static gboolean	gc_cb (gpointer data);


static void	chat_packet_cb (Btp* btp, const void* buffer, 
				 guint length, gpointer user_data);
static void     chat_error_cb (Btp* btp, gpointer user_data);



JMChat* 
jmchat_create (BPeer* bpeer, const gchar* name, const gchar* nick)
{
  JMChat* chat;
  Btp* btp;

  g_return_val_if_fail (name, NULL);

  /* Create a new BTP session */
  btp = btp_create (bpeer, name);
  if (btp == NULL)
    return NULL;

  /* Create a new JMChat */
  chat = g_new0 (JMChat, 1);
  chat->peer = bpeer;
  chat->btp  = btp;
  chat->is_local = TRUE;

  /* Set up URL */
  chat->url = gnet_url_clone (btp_get_url(btp));
  gnet_url_set_protocol (chat->url, "jmchat");

  /* Initialize BTP */
  btp->packet_func = chat_packet_cb;
  btp->error_func  = chat_error_cb;
  btp->packet_user_data = chat;
  btp->error_user_data  = chat;

  /* Send JOIN if I'm a real member */
  if (nick)
    {
      JMChatMember* me;

      /* Add me as member */
      chat->nick = g_strdup (nick);
      me = chat_get_member_update (chat, nick);
      me->is_me = TRUE;

      /* Send JOIN */
      chat_send_join (chat);

      /* Schedule UPDATE */
      set_update_timer (chat);
    }

  /* Schedule garbage collection */
  chat->gc_timer = g_timeout_add (JMCHAT_TIMEOUT_TIME, gc_cb, chat);

  return chat;
}


JMChat*
jmchat_join (BPeer* bpeer, const GURL* url, const gchar* nick)
{
  JMChat* chat;
  Btp* btp;

  g_return_val_if_fail (bpeer, NULL);
  g_return_val_if_fail (url, NULL);

  /* Join the BTP */
  btp = btp_join (bpeer, url);
  if (!btp)
    return NULL;

  /* Create a new JMChat */
  chat = g_new0 (JMChat, 1);
  chat->peer = bpeer;
  chat->url  = gnet_url_clone (url);
  chat->btp  = btp;

  /* Initialize BTP */
  btp->packet_func 	= chat_packet_cb;
  btp->error_func  	= chat_error_cb;
  btp->packet_user_data = chat;
  btp->error_user_data  = chat;

  /* Send JOIN if I'm a real member */
  if (nick)
    {
      JMChatMember* me;

      /* Add me as member */
      chat->nick = g_strdup (nick);
      me = chat_get_member_update (chat, nick);
      me->is_me = TRUE;

      /* Send JOIN */
      chat_send_join (chat);

      /* Schedule UPDATE */
      set_update_timer (chat);
    }

  /* Schedule garbage collection */
  chat->gc_timer = g_timeout_add (JMCHAT_TIMEOUT_TIME, gc_cb, chat);

  return chat;
}



JMChat*	  
jmchat_setup (BPeer* bpeer, const GURL* url, const gchar* nick)
{
  JMChat* chat;

  g_return_val_if_fail (bpeer, NULL);
  g_return_val_if_fail (url, NULL);

  /* Create a new JMChat */
  chat = g_new0 (JMChat, 1);
  chat->peer = bpeer;
  chat->url  = gnet_url_clone (url);

  /* Send JOIN if I'm a real member */
  if (nick)
    {
      JMChatMember* me;

      /* Add me as member */
      chat->nick = g_strdup (nick);
      me = chat_get_member_update (chat, nick);
      me->is_me = TRUE;
    }

  return chat;
}



gboolean
jmchat_rejoin (JMChat* chat)
{
  Btp* btp;

  g_return_val_if_fail (chat, TRUE);
  g_return_val_if_fail (!chat->btp, TRUE);

  /* Join the BTP */
  btp = btp_join (chat->peer, chat->url);
  if (!btp)
    return TRUE;

  chat->btp = btp;

  /* Initialize BTP */
  btp->packet_func = chat_packet_cb;
  btp->error_func  = chat_error_cb;
  btp->packet_user_data = chat;
  btp->error_user_data  = chat;

  /* We aren't down any more */
  chat->is_down = FALSE;

  /* Send JOIN if I'm a real member */
  if (chat->nick)
    {
      JMChatMember* me;

      /* Add me as member */ 
      me = chat_get_member_update (chat, chat->nick);
      me->is_me = TRUE;

      /* Send JOIN */
      chat_send_join (chat);

      /* Schedule UPDATE if we didn't already */
      if (!chat->update_timer)
	 set_update_timer (chat);
    }

  /* Schedule garbage collection if we didn't already */
  if (!chat->gc_timer)
    {
      chat->gc_timer = 
	 g_timeout_add (JMCHAT_TIMEOUT_TIME, gc_cb, chat);
    }

  return FALSE;
}



void
jmchat_leave (JMChat* chat)
{
  g_return_if_fail (chat);

  /* Close the BTP */
  if (chat->btp)
    {
      /* Send a LEAVE if we have a nickname */
      if (chat->nick)
	 chat_send_leave (chat);

      /* Reset the chat */
      chat_reset (chat);
    }
}


void
jmchat_delete (JMChat* chat)
{
  if (!chat)
    return;

  gnet_url_delete (chat->url);
  g_free (chat->nick);
  chat_reset (chat);

  memset (chat, 0, sizeof(*chat));
  g_free (chat);
}


void
jmchat_say (JMChat* chat, gchar* message, gboolean emote)
{
  gchar* buffer;
  guint length;

  g_return_if_fail (chat);
  g_return_if_fail (chat->nick);
  g_return_if_fail (message);

  if (!chat->btp)
    return;

  /* Create a SAY */
  elf_write_format (&buffer, &length, "say", "ssb", 
		     "name", chat->nick, 
		     "message", message,
		     "emote", emote);
  g_return_if_fail (buffer);

  /* Send the SAY */
  btp_send (chat->btp, buffer, length);
  g_free (buffer);
}


JMChatMember*
jmchat_get_member (JMChat* chat, const gchar* nick)
{
  GSList* i;

  g_return_val_if_fail (chat, NULL);
  g_return_val_if_fail (nick, NULL);

  for (i = chat->members; i != NULL; i = i->next)
    {
      JMChatMember* member = (JMChatMember*) i->data;

      if (!strcmp (member->nick, nick))
	 return member;
    }

  return NULL;
}



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

static void
chat_send_join (JMChat* chat)
{
  gchar* buffer;
  guint length;

  g_return_if_fail (chat);
  g_return_if_fail (chat->nick);
  g_return_if_fail (chat->btp);

  elf_write_format (&buffer, &length, "join", "s", 
		     "name", chat->nick);
  btp_send (chat->btp, buffer, length);
  g_free (buffer);
}


static void
chat_send_leave (JMChat* chat)
{
  gchar* buffer;
  guint length;

  g_return_if_fail (chat);
  g_return_if_fail (chat->nick);
  g_return_if_fail (chat->btp);

  elf_write_format (&buffer, &length, "leave", "s", 
		     "name", chat->nick);
  btp_send (chat->btp, buffer, length);
  g_free (buffer);
}


static void
chat_reset (JMChat* chat)
{
  GSList* i;

  g_return_if_fail (chat);

  /* Delete the BTP */
  if (chat->btp)
    {
      btp_leave (chat->btp);
      chat->btp = NULL;
    }

  /* Delete members */
  for (i = chat->members; i != NULL; i = i->next)
    member_delete ((JMChatMember*) i->data);
  g_slist_free (chat->members);
  chat->members = NULL;
  chat->num_members = 0;

  /* Stop updates and garbage collection */
  if (chat->update_timer)
    {
      g_source_remove (chat->update_timer);
      chat->update_timer = 0;
    }
  if (chat->gc_timer)    
    {
      g_source_remove (chat->gc_timer);
      chat->gc_timer = 0;
    }
}


static JMChatMember*
chat_get_member_update (JMChat* chat, const gchar* nick)
{
  JMChatMember* member;

  g_return_val_if_fail (chat, NULL);
  g_return_val_if_fail (nick, NULL);

  /* Get member. */
  member = jmchat_get_member (chat, nick);

  /* If we don't have the member, create it */
  if (!member)
    {
      member = member_new (nick);
      chat->members = g_slist_prepend (chat->members, member);
      ++chat->num_members;
    }

  /* Update last update time */
  Gettimeofday (&member->last_update);

  return member;
}


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

JMChatMember*
member_new (const gchar* nick)
{
  JMChatMember* member;

  g_return_val_if_fail (nick, NULL);

  member = g_new0 (JMChatMember, 1);
  member->nick = g_strdup (nick);

  return member;
}


static void
member_delete (JMChatMember* member)
{
  g_return_if_fail (member);

  g_free (member->nick);
  g_free (member);
}


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

static void
set_update_timer (JMChat* chat)
{
  g_return_if_fail (chat);
  g_return_if_fail (!chat->update_timer);

  chat->update_timer = 	
    g_timeout_add (JMCHAT_UPDATE_TIME, send_update_cb, chat);
}


/* 

   Send an update.  We keep track of when the last update was sent.
   If we sent an update recently, we will delay sending the update.
   This prevents an attacker from flooding the group with updates by
   sending a series of joins (smurf-attack-like).

 */
static void
send_update (JMChat* chat)
{
  Timeval wantsend;
  Timeval maxsend;

  g_return_if_fail (chat);

  /* Cancel any update timer */
  rand_timer_cancel (&chat->update_timer);

  /* Get next possible send time */
  Gettimeofday (&wantsend);
  maxsend = chat->update_tv;
  timeraddmsec (&maxsend, JMCHAT_UPDATE_TIME_MAX);

  /* Check if we can send an update now */
  if (timerleq (&maxsend, &wantsend)) /* maxsend <= wantsend */
    {
      send_update_cb (chat);
      JMCHATP (1, "send_update now\n");
    }

  /* Otherwise, send an update real soon */
  else
    {
      guint time;

      timersub (&maxsend, &wantsend, &wantsend);
      time = wantsend.tv_sec * 1000 + wantsend.tv_usec / 1000;

      rand_timer_set (&chat->update_timer, time, 0,
		      send_update_cb, chat);

      JMCHATP (1, "send_update in %d ms\n", time);
    }
}


static gboolean
send_update_cb (gpointer data)
{
  JMChat* chat = (JMChat*) data;
  gchar* buffer;
  guint length;

  g_return_val_if_fail (chat, FALSE);
  g_return_val_if_fail (chat->btp, FALSE);
  g_return_val_if_fail (chat->nick, FALSE);

  /* Note update time */
  Gettimeofday (&chat->update_tv);

  /* Send update */
  elf_write_format (&buffer, &length, "update", "s", 
		    "name", chat->nick);
  btp_send (chat->btp, buffer, length);
  g_free (buffer);

  /* Update timer */
  chat->update_timer = 0;
  set_update_timer (chat);

  return FALSE;
}


static gboolean
gc_cb (gpointer data)
{
  JMChat* chat = (JMChat*) data;
  struct timeval timeofday;
  GSList* i;

  g_return_val_if_fail (chat, FALSE);

  /* gettimeofday */
  Gettimeofday (&timeofday);

  for (i = chat->members; i != NULL; )
    {
      GSList* old_i = i; 
      JMChatMember* member = (JMChatMember*) i->data;
      struct timeval diff;

      /* Advance i now since we might delete it */
      i = i->next;

      /* Don't garbage collect me */
      if (member->is_me)
	continue;

      /* Remove if timed out */
      timersub(&timeofday, &member->last_update, &diff);
      if (diff.tv_sec > (JMCHAT_TIMEOUT_TIME / 1000))
	{
	  /* Remove from list */
	  chat->members = g_slist_remove_link (chat->members, old_i);
	  --chat->num_members;

	  /* Notify upper level */
	  if (chat->info_func)
	    chat->info_func (chat, member, JMCHAT_LEAVE_TIMEOUT, 
			     NULL, NULL, chat->user_data);

	  /* Delete member */
	  member_delete (member);
	}
    }

  return TRUE;
}



static void
chat_packet_cb (Btp* btp, const void* buffer, guint length, gpointer user_data)
{
  JMChat* chat = (JMChat*) user_data;
  GList* nodes;
  GList* i;

  nodes = elf_read_list (buffer, length);
  for (i = nodes; i != NULL; i = i->next)
    {
      ElfNode* node = (ElfNode*) i->data;
      gchar* nick;

      /* Get name.  If no name, skip.  All JMCP messages have name as
         a parameter. */
      nick = elf_get_attribute (node, "name");
      if (!nick) continue;

      if (!strcmp(node->name, "join"))
	{
	  JMChatMember* member = NULL;

	  /* Get member */
	  member = chat_get_member_update (chat, nick);

	  /* Notify upper layer */
	  if (chat->info_func)
	    (chat->info_func) (chat, member, JMCHAT_JOIN, 
			       NULL, NULL, chat->user_data);

	  /* Send update now */
	  if (chat->nick)
	    send_update (chat);
	}

      else if (!strcmp(node->name, "leave"))
	{
	  JMChatMember* member = NULL;

	  /* Get the member */
	  member = chat_get_member_update (chat, nick);

	  /* Notify upper layer */
	  if (chat->info_func)
	    (chat->info_func) (chat, member, JMCHAT_LEAVE, 
			       NULL, NULL, chat->user_data);

	  /* Delete the member (only if it isn't me) */
	  if (!member->is_me)
	    {
	      chat->members = g_slist_remove (chat->members, member);
	      --chat->num_members;
	      member_delete (member);
	    }
	}

      else if (!strcmp(node->name, "update"))
	{
	  JMChatMember* member = NULL;

	  /* Get member */
	  member = chat_get_member_update (chat, nick);

	  /* Notify upper layer */
	  if (chat->info_func)
	    (chat->info_func) (chat, member, JMCHAT_UPDATE, 
			       NULL, NULL, chat->user_data);
	}

      else if (!strcmp(node->name, "say"))
	{
	  JMChatMember* member = NULL;
	  gchar* msg;
	  gboolean emote;

	  /* Get the member */
	  member = chat_get_member_update (chat, nick);

	  /* Get the message */
	  msg = elf_get_attribute (node, "message");
	  if (!msg) continue;
	  
	  /* Get emote flag */
	  emote = elf_get_attribute_bool (node, "emote");

	  if (chat->info_func)
	    (chat->info_func) (chat, member, JMCHAT_SAY, 
			       msg, emote? (gpointer) 0x1 : NULL, 
			       chat->user_data);
	}
    }

  elf_delete_list (nodes);
}


static void
chat_error_cb (Btp* btp, gpointer user_data)
{
  JMChat* chat = (JMChat*) user_data;

  chat_reset (chat);
  chat->is_down = TRUE;
  
  if (chat->error_func)
    chat->error_func (chat, chat->user_data);
}


syntax highlighted by Code2HTML, v. 0.9.1