/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
 * Copyright (c) 2007-2009 Strasbourg University
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation;
 *
 * 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
 *
 * Author: Sebastien Vincent <vincent@clarinet.u-strasbg.fr>
 */

#include "ns3/log.h"
#include "ns3/uinteger.h"
#include "ns3/node.h"

#include "ipv6-l3-protocol.h" 
#include "icmpv6-l4-protocol.h"
#include "ndisc-cache.h"
#include "ipv6-interface.h"

namespace ns3
{

NS_LOG_COMPONENT_DEFINE ("NdiscCache");

NS_OBJECT_ENSURE_REGISTERED (NdiscCache);

TypeId NdiscCache::GetTypeId ()
{
  static TypeId tid = TypeId ("ns3::NdiscCache")
    .SetParent<Object> ()
    .AddAttribute ("UnresolvedQueueSize",
                   "Size of the queue for packets pending an NA reply.",
                   UintegerValue (DEFAULT_UNRES_QLEN),
                   MakeUintegerAccessor (&NdiscCache::m_unresQlen),
                   MakeUintegerChecker<uint32_t> ())
  ;
  return tid;
} 

NdiscCache::NdiscCache ()
{
  NS_LOG_FUNCTION_NOARGS ();
}

NdiscCache::~NdiscCache ()
{
  NS_LOG_FUNCTION_NOARGS ();
  Flush ();
}

void NdiscCache::DoDispose ()
{
  NS_LOG_FUNCTION_NOARGS ();
  Flush ();
  m_device = 0;
  m_interface = 0;
  Object::DoDispose ();
}

void NdiscCache::SetDevice (Ptr<NetDevice> device, Ptr<Ipv6Interface> interface)
{
  NS_LOG_FUNCTION (this << device << interface);
  m_device = device;
  m_interface = interface;
}

Ptr<Ipv6Interface> NdiscCache::GetInterface () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_interface;
}

Ptr<NetDevice> NdiscCache::GetDevice () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_device;
}

NdiscCache::Entry* NdiscCache::Lookup (Ipv6Address dst)
{
  NS_LOG_FUNCTION (this << dst);

  if (m_ndCache.find (dst) != m_ndCache.end ())
    {
      NdiscCache::Entry* entry = m_ndCache[dst];
      return entry;
    }
  return 0;
}

NdiscCache::Entry* NdiscCache::Add (Ipv6Address to)
{
  NS_LOG_FUNCTION (this << to);
  NS_ASSERT (m_ndCache.find (to) == m_ndCache.end ());

  NdiscCache::Entry* entry = new NdiscCache::Entry (this);
  entry->SetIpv6Address (to);
  m_ndCache[to] = entry;
  return entry;
}

void NdiscCache::Remove (NdiscCache::Entry* entry)
{
  NS_LOG_FUNCTION_NOARGS ();

  for (CacheI i = m_ndCache.begin (); i != m_ndCache.end (); i++)
    {
      if ((*i).second == entry)
        {
          m_ndCache.erase (i);
          entry->ClearWaitingPacket ();
          delete entry;
          return;
        }
    }
}

void NdiscCache::Flush ()
{
  NS_LOG_FUNCTION_NOARGS ();

  for (CacheI i = m_ndCache.begin (); i != m_ndCache.end (); i++)
    {
      delete (*i).second; /* delete the pointer NdiscCache::Entry */
    }

  m_ndCache.erase (m_ndCache.begin (), m_ndCache.end ());
}

void NdiscCache::SetUnresQlen (uint32_t unresQlen)
{
  NS_LOG_FUNCTION (this << unresQlen);
  m_unresQlen = unresQlen;
}

uint32_t NdiscCache::GetUnresQlen ()
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_unresQlen;
}

NdiscCache::Entry::Entry (NdiscCache* nd)
  : m_ndCache (nd),
    m_waiting (),
    m_router (false),
    m_reachableTimer (Timer::CANCEL_ON_DESTROY),
    m_retransTimer (Timer::CANCEL_ON_DESTROY),
    m_probeTimer (Timer::CANCEL_ON_DESTROY),
    m_delayTimer (Timer::CANCEL_ON_DESTROY),
    m_lastReachabilityConfirmation (Seconds (0.0)),
    m_nsRetransmit (0)
{
  NS_LOG_FUNCTION_NOARGS ();
}

void NdiscCache::Entry::SetRouter (bool router)
{
  NS_LOG_FUNCTION (this << router);
  m_router = router;
}

bool NdiscCache::Entry::IsRouter () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_router;
}

void NdiscCache::Entry::AddWaitingPacket (Ptr<Packet> p)
{
  NS_LOG_FUNCTION (this << p);

  if (m_waiting.size () >= m_ndCache->GetUnresQlen ())
    {
      /* we store only m_unresQlen packet => first packet in first packet remove */
      /* XXX report packet as 'dropped' */
      m_waiting.remove (0);
    }
  m_waiting.push_back (p);
}

void NdiscCache::Entry::ClearWaitingPacket ()
{
  NS_LOG_FUNCTION_NOARGS ();
  /* XXX report packets as 'dropped' */
  m_waiting.clear ();
}

void NdiscCache::Entry::FunctionReachableTimeout ()
{
  NS_LOG_FUNCTION_NOARGS ();
  this->MarkStale ();
}

void NdiscCache::Entry::FunctionRetransmitTimeout ()
{
  NS_LOG_FUNCTION_NOARGS ();
  Ptr<Icmpv6L4Protocol> icmpv6 = m_ndCache->GetDevice ()->GetNode ()->GetObject<Ipv6L3Protocol> ()->GetIcmpv6 ();
  Ipv6Address addr;

  /* determine source address */
  if (m_ipv6Address.IsLinkLocal ())
    {
      addr = m_ndCache->GetInterface ()->GetLinkLocalAddress ().GetAddress ();;
    }
  else if (!m_ipv6Address.IsAny ())
    {
      addr = m_ndCache->GetInterface ()->GetAddressMatchingDestination (m_ipv6Address).GetAddress ();

      if (addr.IsAny ()) /* maybe address has expired */
        {
          /* delete the entry */
          m_ndCache->Remove (this);
          return;
        }
    }

  if (GetNSRetransmit () < icmpv6->MAX_MULTICAST_SOLICIT)
    {
      IncNSRetransmit ();

      icmpv6->SendNS (addr, Ipv6Address::MakeSolicitedAddress (m_ipv6Address), m_ipv6Address, m_ndCache->GetDevice ()->GetAddress ());
      /* arm the timer again */
      StartRetransmitTimer ();
    }
  else
    {
      Ptr<Packet> malformedPacket = m_waiting.front ();
      if (malformedPacket == 0)
        {
          malformedPacket = Create<Packet> ();
        }

      icmpv6->SendErrorDestinationUnreachable (malformedPacket, addr, Icmpv6Header::ICMPV6_ADDR_UNREACHABLE);

      /* delete the entry */
      m_ndCache->Remove (this);
    }
}

void NdiscCache::Entry::FunctionDelayTimeout ()
{
  NS_LOG_FUNCTION_NOARGS ();
  Ptr<Ipv6L3Protocol> ipv6 = m_ndCache->GetDevice ()->GetNode ()->GetObject<Ipv6L3Protocol> ();
  Ptr<Icmpv6L4Protocol> icmpv6 = ipv6->GetIcmpv6 ();
  Ipv6Address addr;

  this->MarkProbe ();

  if (m_ipv6Address.IsLinkLocal ())
    {
      addr = m_ndCache->GetInterface ()->GetLinkLocalAddress ().GetAddress ();
    }
  else if (!m_ipv6Address.IsAny ())
    {
      addr = m_ndCache->GetInterface ()->GetAddressMatchingDestination (m_ipv6Address).GetAddress ();
      if (addr.IsAny ()) /* maybe address has expired */
        {
          /* delete the entry */
          m_ndCache->Remove (this);
          return;
        }
    }
  else
    {
      /* should not happen */
      return;
    }

  Ptr<Packet> p = icmpv6->ForgeNS (addr, m_ipv6Address, m_ipv6Address, m_ndCache->GetDevice ()->GetAddress ());
  m_ndCache->GetDevice ()->Send (p, this->GetMacAddress (), Ipv6L3Protocol::PROT_NUMBER);

  ResetNSRetransmit ();
  IncNSRetransmit ();
  StartProbeTimer ();
}

void NdiscCache::Entry::FunctionProbeTimeout ()
{
  NS_LOG_FUNCTION_NOARGS ();
  Ptr<Ipv6L3Protocol> ipv6 = m_ndCache->GetDevice ()->GetNode ()->GetObject<Ipv6L3Protocol> ();
  Ptr<Icmpv6L4Protocol> icmpv6 = ipv6->GetIcmpv6 ();

  if (GetNSRetransmit () < icmpv6->MAX_UNICAST_SOLICIT)
    {
      Ipv6Address addr;

      if (m_ipv6Address.IsLinkLocal ())
        {
          addr = m_ndCache->GetInterface ()->GetLinkLocalAddress ().GetAddress ();
        }
      else if (!m_ipv6Address.IsAny ())
        {
          addr = m_ndCache->GetInterface ()->GetAddressMatchingDestination (m_ipv6Address).GetAddress ();
          if (addr.IsAny ()) /* maybe address has expired */
            {
              /* delete the entry */
              m_ndCache->Remove (this);
              return;
            }
        }
      else
        {
          /* should not happen */
          return;
        }

      IncNSRetransmit ();
      /* icmpv6->SendNS (m_ndCache->GetInterface ()->GetLinkLocalAddress (), m_ipv6Address, m_ipv6Address, m_ndCache->GetDevice ()->GetAddress ()); */
      Ptr<Packet> p = icmpv6->ForgeNS (addr, m_ipv6Address, m_ipv6Address, m_ndCache->GetDevice ()->GetAddress ());
      m_ndCache->GetDevice ()->Send (p, this->GetMacAddress (), Ipv6L3Protocol::PROT_NUMBER);

      /* arm the timer again */
      StartProbeTimer ();
    }
  else
    {
      /* delete the entry */
      m_ndCache->Remove (this);
    }
}

void NdiscCache::Entry::SetIpv6Address (Ipv6Address ipv6Address)
{
  NS_LOG_FUNCTION (this << ipv6Address);
  m_ipv6Address = ipv6Address;
}

uint8_t NdiscCache::Entry::GetNSRetransmit () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_nsRetransmit;
}

void NdiscCache::Entry::IncNSRetransmit ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_nsRetransmit++;
}

void NdiscCache::Entry::ResetNSRetransmit ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_nsRetransmit = 0;
}

Time NdiscCache::Entry::GetLastReachabilityConfirmation () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_lastReachabilityConfirmation;
}

void NdiscCache::Entry::UpdateLastReachabilityconfirmation ()
{
  NS_LOG_FUNCTION_NOARGS ();
}

void NdiscCache::Entry::StartReachableTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_reachableTimer.SetFunction (&NdiscCache::Entry::FunctionReachableTimeout, this);
  m_reachableTimer.SetDelay (MilliSeconds (Icmpv6L4Protocol::REACHABLE_TIME));
  m_reachableTimer.Schedule ();
}

void NdiscCache::Entry::StopReachableTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_reachableTimer.Cancel ();
}

void NdiscCache::Entry::StartProbeTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_probeTimer.SetFunction (&NdiscCache::Entry::FunctionProbeTimeout, this);
  m_probeTimer.SetDelay (MilliSeconds (Icmpv6L4Protocol::RETRANS_TIMER));
  m_probeTimer.Schedule ();
}

void NdiscCache::Entry::StopProbeTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_probeTimer.Cancel ();
  ResetNSRetransmit ();
}


void NdiscCache::Entry::StartDelayTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_delayTimer.SetFunction (&NdiscCache::Entry::FunctionDelayTimeout, this);
  m_delayTimer.SetDelay (Seconds (Icmpv6L4Protocol::DELAY_FIRST_PROBE_TIME));
  m_delayTimer.Schedule ();
}

void NdiscCache::Entry::StopDelayTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_delayTimer.Cancel ();
  ResetNSRetransmit ();
}

void NdiscCache::Entry::StartRetransmitTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_retransTimer.SetFunction (&NdiscCache::Entry::FunctionRetransmitTimeout, this);
  m_retransTimer.SetDelay (MilliSeconds (Icmpv6L4Protocol::RETRANS_TIMER));
  m_retransTimer.Schedule ();
}

void NdiscCache::Entry::StopRetransmitTimer ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_retransTimer.Cancel ();
  ResetNSRetransmit ();
}

void NdiscCache::Entry::MarkIncomplete (Ptr<Packet> p)
{
  NS_LOG_FUNCTION (this << p);
  m_state = INCOMPLETE;

  if (p)
    {
      m_waiting.push_back (p);
    }
}

std::list<Ptr<Packet> > NdiscCache::Entry::MarkReachable (Address mac)
{
  NS_LOG_FUNCTION (this << mac);
  m_state = REACHABLE;
  m_macAddress = mac;
  return m_waiting;
}

void NdiscCache::Entry::MarkProbe ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_state = PROBE;
}

void NdiscCache::Entry::MarkStale ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_state = STALE;
}

void NdiscCache::Entry::MarkReachable ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_state = REACHABLE;
}

std::list<Ptr<Packet> > NdiscCache::Entry::MarkStale (Address mac)
{
  NS_LOG_FUNCTION (this << mac);
  m_state = STALE;
  m_macAddress = mac;
  return m_waiting;
}

void NdiscCache::Entry::MarkDelay ()
{
  NS_LOG_FUNCTION_NOARGS ();
  m_state = DELAY;
}

bool NdiscCache::Entry::IsStale () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return (m_state == STALE);
}

bool NdiscCache::Entry::IsReachable () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return (m_state == REACHABLE);
}

bool NdiscCache::Entry::IsDelay () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return (m_state == DELAY);
}

bool NdiscCache::Entry::IsIncomplete () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return (m_state == INCOMPLETE);
}

bool NdiscCache::Entry::IsProbe () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return (m_state == PROBE);
}

Address NdiscCache::Entry::GetMacAddress () const
{
  NS_LOG_FUNCTION_NOARGS ();
  return m_macAddress;
}

void NdiscCache::Entry::SetMacAddress (Address mac)
{
  NS_LOG_FUNCTION (this << mac);
  m_macAddress = mac;
}

} /* namespace ns3 */

