/**
 * @file igmp.c
 * @brief IGMP (Internet Group Management Protocol)
 *
 * @section License
 *
 * Copyright (C) 2010-2014 Oryx Embedded. All rights reserved.
 *
 * This file is part of CycloneTCP Open.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * @section Description
 *
 * IGMP is used by IP hosts to report their multicast group memberships
 * to routers. Refer to the following RFCs for complete details:
 * - RFC 1112: Host Extensions for IP Multicasting
 * - RFC 2236: Internet Group Management Protocol, Version 2
 * - RFC 3376: Internet Group Management Protocol, Version 3
 *
 * @author Oryx Embedded (www.oryx-embedded.com)
 * @version 1.4.4
 **/

//Switch to the appropriate trace level
#define TRACE_LEVEL IGMP_TRACE_LEVEL

//Dependencies
#include "tcp_ip_stack.h"
#include "ip.h"
#include "ipv4.h"
#include "igmp.h"
#include "debug.h"

//Check TCP/IP stack configuration
#if (IPV4_SUPPORT == ENABLED && IGMP_SUPPORT == ENABLED)


/**
 * @brief IGMP initialization
 * @param[in] interface Underlying network interface
 * @return Error code
 **/

error_t igmpInit(NetInterface *interface)
{
   //The default host compatibility mode is IGMPv2
   interface->igmpv1RouterPresent = FALSE;

   //Start IGMPv1 router present timer
   interface->igmpv1RouterPresentTimer =
      osGetTickCount() + IGMP_V1_ROUTER_PRESENT_TIMEOUT;

   //Successful initialization
   return NO_ERROR;
}


/**
 * @brief Join the specified host group
 * @param[in] interface Underlying network interface
 * @param[in] entry IPv4 filter entry identifying the host group to join
 * @return Error code
 **/

error_t igmpJoinGroup(NetInterface *interface, Ipv4FilterEntry *entry)
{
   //The all-systems group (address 224.0.0.1) is handled as a special
   //case.  The host starts in Idle Member state for that group on every
   //interface and never transitions to another state
   if(entry->addr == IGMP_ALL_SYSTEMS_ADDR)
   {
      //Clear flag
      entry->flag = FALSE;
      //Enter the Idle Member state
      entry->state = IGMP_STATE_IDLE_MEMBER;
   }
   else
   {
      //Link is up?
      if(interface->linkState)
      {
         //When a host joins a multicast group, it should immediately transmit
         //an unsolicited Membership Report for that group
         igmpSendReportMessage(interface, entry->addr);

         //Set flag
         entry->flag = TRUE;
         //Start timer
         entry->timer = osGetTickCount() + IGMP_UNSOLICITED_REPORT_INTERVAL;
         //Enter the Delaying Member state
         entry->state = IGMP_STATE_DELAYING_MEMBER;
      }
      //Link is down?
      else
      {
         //Clear flag
         entry->flag = FALSE;
         //Enter the Idle Member state
         entry->state = IGMP_STATE_IDLE_MEMBER;
      }
   }

   //Successful processing
   return NO_ERROR;
}


/**
 * @brief Leave the specified host group
 * @param[in] interface Underlying network interface
 * @param[in] entry IPv4 filter entry identifying the host group to leave
 * @return Error code
 **/

error_t igmpLeaveGroup(NetInterface *interface, Ipv4FilterEntry *entry)
{
   //Check link state
   if(interface->linkState)
   {
      //Send a Leave Group message if the flag is set
      if(entry->flag)
         igmpSendLeaveGroupMessage(interface, entry->addr);
   }

   //Switch to the Non-Member state
   entry->state = IGMP_STATE_NON_MEMBER;

   //Successful processing
   return NO_ERROR;
}


/**
 * @brief IGMP timer handler
 *
 * This routine must be periodically called by the TCP/IP stack to
 * handle IGMP related timers
 *
 * @param[in] interface Underlying network interface
 **/

void igmpTick(NetInterface *interface)
{
   uint_t i;
   systime_t time;
   Ipv4FilterEntry *entry;

   //Get current time
   time = osGetTickCount();

   //Check IGMPv1 router present timer
   if(timeCompare(time, interface->igmpv1RouterPresentTimer) >= 0)
      interface->igmpv1RouterPresent = FALSE;

   //Acquire exclusive access to the IPv4 filter table
   osMutexAcquire(interface->ipv4FilterMutex);

   //Loop through filter table entries
   for(i = 0; i < interface->ipv4FilterSize; i++)
   {
      //Point to the current entry
      entry = &interface->ipv4Filter[i];

      //Delaying Member state?
      if(entry->state == IGMP_STATE_DELAYING_MEMBER)
      {
         //Timer expired?
         if(timeCompare(time, entry->timer) >= 0)
         {
            //Send a Membership Report message for the group on the interface
            igmpSendReportMessage(interface, entry->addr);

            //Set flag
            entry->flag = TRUE;
            //Switch to the Idle Member state
            entry->state = IGMP_STATE_IDLE_MEMBER;
         }
      }
   }

   //Release exclusive access to the IPv4 filter table
   osMutexRelease(interface->ipv4FilterMutex);
}


/**
 * @brief Callback function for link change event
 * @param[in] interface Underlying network interface
 **/

void igmpLinkChangeEvent(NetInterface *interface)
{
   uint_t i;
   systime_t time;
   Ipv4FilterEntry *entry;

   //Get current time
   time = osGetTickCount();

   //Acquire exclusive access to the IPv4 filter table
   osMutexAcquire(interface->ipv4FilterMutex);

   //Link up event?
   if(interface->linkState)
   {
      //The default host compatibility mode is IGMPv2
      interface->igmpv1RouterPresent = FALSE;
      //Start IGMPv1 router present timer
      interface->igmpv1RouterPresentTimer = time + IGMP_V1_ROUTER_PRESENT_TIMEOUT;

      //Loop through filter table entries
      for(i = 0; i < interface->ipv4FilterSize; i++)
      {
         //Point to the current entry
         entry = &interface->ipv4Filter[i];

         //The all-systems group (address 224.0.0.1) is handled as a special
         //case.  The host starts in Idle Member state for that group on every
         //interface and never transitions to another state
         if(entry->addr == IGMP_ALL_SYSTEMS_ADDR)
            continue;

         //Send an unsolicited Membership Report for that group
         igmpSendReportMessage(interface, entry->addr);

         //Set flag
         entry->flag = TRUE;
         //Start timer
         entry->timer = time + IGMP_UNSOLICITED_REPORT_INTERVAL;
         //Enter the Delaying Member state
         entry->state = IGMP_STATE_DELAYING_MEMBER;
      }
   }
   //Link down event?
   else
   {
      //Loop through filter table entries
      for(i = 0; i < interface->ipv4FilterSize; i++)
      {
         //Point to the current entry
         entry = &interface->ipv4Filter[i];

         //Clear flag
         entry->flag = FALSE;
         //Enter the Idle Member state
         entry->state = IGMP_STATE_IDLE_MEMBER;
      }
   }

   //Release exclusive access to the IPv4 filter table
   osMutexRelease(interface->ipv4FilterMutex);
}


/**
 * @brief Process incoming IGMP message
 * @param[in] interface Underlying network interface
 * @param[in] buffer Multi-part buffer containing the incoming IGMP message
 * @param[in] offset Offset to the first byte of the IGMP message
 **/

void igmpProcessMessage(NetInterface *interface,
   const ChunkedBuffer *buffer, size_t offset)
{
   size_t length;
   IgmpMessage *message;

   //Retrieve the length of the IGMP message
   length = chunkedBufferGetLength(buffer) - offset;

   //Ensure the message length is correct
   if(length < sizeof(IgmpMessage))
   {
      //Debug message
      TRACE_WARNING("IGMP message length is invalid!\r\n");
      //Silently discard incoming message
      return;
   }

   //Point to the beginning of the IGMP message
   message = chunkedBufferAt(buffer, offset);
   //Sanity check
   if(!message) return;

   //Debug message
   TRACE_INFO("IGMP message received (%" PRIuSIZE " bytes)...\r\n", length);
   //Dump message contents for debugging purpose
   igmpDumpMessage(message);

   //Verify checksum value
   if(ipCalcChecksumEx(buffer, offset, length) != 0x0000)
   {
      //Debug message
      TRACE_WARNING("Wrong IGMP header checksum!\r\n");
      //Drop incoming message
      return;
   }

   //Check the type field
   switch(message->type)
   {
   //Membership Query message?
   case IGMP_TYPE_MEMBERSHIP_QUERY:
      //Process Membership Query message
      igmpProcessQueryMessage(interface, message, length);
      break;
   //Membership Report message?
   case IGMP_TYPE_MEMBERSHIP_REPORT_V1:
   case IGMP_TYPE_MEMBERSHIP_REPORT_V2:
      //Process Membership Query message
      igmpProcessReportMessage(interface, message, length);
      break;
   //Unknown type?
   default:
      //Debug message
      TRACE_WARNING("Unknown IGMP message type!\r\n");
      //Discard incoming IGMP message
      break;
   }
}


/**
 * @brief Process incoming Membership Query message
 * @param[in] interface Underlying network interface
 * @param[in] message Incoming Membership Query message
 * @param[in] length Message length
 **/

void igmpProcessQueryMessage(NetInterface *interface,
   const IgmpMessage *message, size_t length)
{
   uint_t i;
   systime_t time;
   systime_t maxRespTime;
   Ipv4FilterEntry *entry;

   //Get current time
   time = osGetTickCount();

   //IGMPv1 Membership Query message?
   if(message->maxRespTime == 0)
   {
      //The host receives a query with the Max Response Time field set to 0
      interface->igmpv1RouterPresent = TRUE;
      //Restart IGMPv1 router present timer
      interface->igmpv1RouterPresentTimer = time + IGMP_V1_ROUTER_PRESENT_TIMEOUT;
      //The maximum response time is 10 seconds by default
      maxRespTime = IGMP_V1_MAX_RESPONSE_TIME;
   }
   //IGMPv2 Membership Query message?
   else
   {
      //The Max Resp Time field specifies the maximum time allowed
      //before sending a responding report
      maxRespTime = message->maxRespTime * 10;
   }

   //Acquire exclusive access to the IPv4 filter table
   osMutexAcquire(interface->ipv4FilterMutex);

   //Loop through filter table entries
   for(i = 0; i < interface->ipv4FilterSize; i++)
   {
      //Point to the current entry
      entry = &interface->ipv4Filter[i];

      //The all-systems group (224.0.0.1) is handled as a special case. The
      //host starts in Idle Member state for that group on every interface
      //and never transitions to another state
      if(entry->addr == IGMP_ALL_SYSTEMS_ADDR)
         continue;

      //A General Query applies to all memberships on the interface from which
      //the Query is received.  A Group-Specific Query applies to membership
      //in a single group on the interface from which the Query is received
      if(message->groupAddr == IPV4_UNSPECIFIED_ADDR ||
         message->groupAddr == entry->addr)
      {
         //Delaying Member state?
         if(entry->state == IGMP_STATE_DELAYING_MEMBER)
         {
            //The timer has not yet expired?
            if(timeCompare(time, entry->timer) < 0)
            {
               //If a timer for the group is already running, it is reset to
               //the random value only if the requested Max Response Time is
               //less than the remaining value of the running timer
               if(maxRespTime < (entry->timer - time))
               {
                  //Restart delay timer
                  entry->timer = time + igmpRand(maxRespTime);
               }
            }
         }
         //Idle Member state?
         else if(entry->state == IGMP_STATE_IDLE_MEMBER)
         {
            //Switch to the Delaying Member state
            entry->state = IGMP_STATE_DELAYING_MEMBER;
            //Delay the response by a random amount of time
            entry->timer = time + igmpRand(maxRespTime);
         }
      }
   }

   //Release exclusive access to the IPv4 filter table
   osMutexRelease(interface->ipv4FilterMutex);
}


/**
 * @brief Process incoming Membership Report message
 * @param[in] interface Underlying network interface
 * @param[in] message Incoming Membership Report message
 * @param[in] length Message length
 **/

void igmpProcessReportMessage(NetInterface *interface,
   const IgmpMessage *message, size_t length)
{
   uint_t i;
   Ipv4FilterEntry *entry;

   //Acquire exclusive access to the IPv4 filter table
   osMutexAcquire(interface->ipv4FilterMutex);

   //Loop through filter table entries
   for(i = 0; i < interface->ipv4FilterSize; i++)
   {
      //Point to the current entry
      entry = &interface->ipv4Filter[i];

      //Report messages are ignored for memberships in
      //the Non-Member or Idle Member state
      if(entry->state == IGMP_STATE_DELAYING_MEMBER)
      {
         //The Membership Report message matches the current entry?
         if(message->groupAddr == entry->addr)
         {
            //Clear flag
            entry->flag = FALSE;
            //Switch to the Idle Member state
            entry->state = IGMP_STATE_IDLE_MEMBER;
         }
      }
   }

   //Release exclusive access to the IPv4 filter table
   osMutexRelease(interface->ipv4FilterMutex);
}


/**
 * @brief Send Membership Report message
 * @param[in] interface Underlying network interface
 * @param[in] ipAddr IPv4 address specifying the group address
 * @return Error code
 **/

error_t igmpSendReportMessage(NetInterface *interface, Ipv4Addr ipAddr)
{
   error_t error;
   size_t offset;
   IgmpMessage *message;
   ChunkedBuffer *buffer;
   Ipv4PseudoHeader pseudoHeader;

   //Make sure the specified address is a valid multicast address
   if(!ipv4IsMulticastAddr(ipAddr))
      return ERROR_INVALID_ADDRESS;

   //The all-systems group (224.0.0.1) is handled as a special case.
   //The host never sends a report for that group
   if(ipAddr == IGMP_ALL_SYSTEMS_ADDR)
      return ERROR_INVALID_ADDRESS;

   //Allocate a memory buffer to hold an IGMP message
   buffer = ipAllocBuffer(sizeof(IgmpMessage), &offset);
   //Failed to allocate memory?
   if(!buffer) return ERROR_OUT_OF_MEMORY;

   //Point to the beginning of the IGMP message
   message = chunkedBufferAt(buffer, offset);

   //The type of report is determined by the state of the interface
   if(interface->igmpv1RouterPresent)
      message->type = IGMP_TYPE_MEMBERSHIP_REPORT_V1;
   else
      message->type = IGMP_TYPE_MEMBERSHIP_REPORT_V2;

   //Format the Membership Report message
   message->maxRespTime = 0;
   message->checksum = 0;
   message->groupAddr = ipAddr;

   //Message checksum calculation
   message->checksum = ipCalcChecksumEx(buffer, offset, sizeof(IgmpMessage));

   //Format IPv4 pseudo header
   pseudoHeader.srcAddr = interface->ipv4Config.addr;
   pseudoHeader.destAddr = ipAddr;
   pseudoHeader.reserved = 0;
   pseudoHeader.protocol = IPV4_PROTOCOL_IGMP;
   pseudoHeader.length = HTONS(sizeof(IgmpMessage));

   //Debug message
   TRACE_INFO("Sending IGMP message (%" PRIuSIZE " bytes)...\r\n", sizeof(IgmpMessage));
   //Dump message contents for debugging purpose
   igmpDumpMessage(message);

   //The Membership Report message is sent to the group being reported
   error = ipv4SendDatagram(interface, &pseudoHeader, buffer, offset, IGMP_TTL);

   //Free previously allocated memory
   chunkedBufferFree(buffer);
   //Return status code
   return error;
}


/**
 * @brief Send Leave Group message
 * @param[in] interface Underlying network interface
 * @param[in] ipAddr IPv4 address specifying the group address being left
 * @return Error code
 **/

error_t igmpSendLeaveGroupMessage(NetInterface *interface, Ipv4Addr ipAddr)
{
   error_t error;
   size_t offset;
   ChunkedBuffer *buffer;
   IgmpMessage *message;
   Ipv4PseudoHeader pseudoHeader;

   //Make sure the specified address is a valid multicast address
   if(!ipv4IsMulticastAddr(ipAddr))
      return ERROR_INVALID_ADDRESS;

   //The all-systems group (224.0.0.1) is handled as a special case.
   //The host never sends a Leave Group message for that group
   if(ipAddr == IGMP_ALL_SYSTEMS_ADDR)
      return ERROR_INVALID_ADDRESS;

   //If the interface state says the querier is running
   //IGMPv1, this action should be skipped
   if(interface->igmpv1RouterPresent)
      return NO_ERROR;

   //Allocate a memory buffer to hold an IGMP message
   buffer = ipAllocBuffer(sizeof(IgmpMessage), &offset);
   //Failed to allocate memory?
   if(!buffer) return ERROR_OUT_OF_MEMORY;

   //Point to the beginning of the IGMP message
   message = chunkedBufferAt(buffer, offset);

   //Format the Leave Group message
   message->type = IGMP_TYPE_LEAVE_GROUP;
   message->maxRespTime = 0;
   message->checksum = 0;
   message->groupAddr = ipAddr;

   //Message checksum calculation
   message->checksum = ipCalcChecksumEx(buffer, offset, sizeof(IgmpMessage));

   //Format IPv4 pseudo header
   pseudoHeader.srcAddr = interface->ipv4Config.addr;
   pseudoHeader.destAddr = IGMP_ALL_ROUTERS_ADDR;
   pseudoHeader.reserved = 0;
   pseudoHeader.protocol = IPV4_PROTOCOL_IGMP;
   pseudoHeader.length = HTONS(sizeof(IgmpMessage));

   //Debug message
   TRACE_INFO("Sending IGMP message (%" PRIuSIZE " bytes)...\r\n", sizeof(IgmpMessage));
   //Dump message contents for debugging purpose
   igmpDumpMessage(message);

   //The Leave Group message is sent to the all-routers multicast group
   error = ipv4SendDatagram(interface, &pseudoHeader, buffer, offset, IGMP_TTL);

   //Free previously allocated memory
   chunkedBufferFree(buffer);
   //Return status code
   return error;
}


/**
 * @brief Get a random value in the specified range
 * @param[in] max Upper bound
 * @return Random value in the specified range
 **/

uint32_t igmpRand(uint32_t max)
{
   //Return a random value in the given range
   return tcpIpStackGetRand() % (max + 1);
}


/**
 * @brief Dump IGMP message for debugging purpose
 * @param[in] message Pointer to the IGMP message
 **/

void igmpDumpMessage(const IgmpMessage *message)
{
   //Dump IGMP message
   TRACE_DEBUG("  Type = 0x%02" PRIX8 "\r\n", message->type);
   TRACE_DEBUG("  Max Resp Time = 0x%02" PRIX8 "\r\n", message->maxRespTime);
   TRACE_DEBUG("  Checksum = 0x%04" PRIX16 "\r\n", ntohs(message->checksum));
   TRACE_DEBUG("  Group Address = %s\r\n", ipv4AddrToString(message->groupAddr, NULL));
}

#endif
