package brn.swans.route;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.runtime.Util;
import jist.swans.Constants;
import jist.swans.Node;
import jist.swans.mac.MacAddress;
import jist.swans.misc.Message;
import jist.swans.misc.MessageAnno;
import jist.swans.misc.Protocol;
import jist.swans.net.NetAddress;
import jist.swans.net.NetInterface;
import jist.swans.net.NetMessage;
import jist.swans.net.NetInterface.NetHandler;
import jist.swans.net.NetMessage.Ip;
import jist.swans.radio.RadioInterface;
import jist.swans.radio.RadioInterface.RFChannel;
import jist.swans.route.AbstractRoute;

import org.apache.log4j.Logger;

import brn.swans.route.metric.ArpTableInterface;
import brn.swans.route.metric.RouteMetricInterface;
import brn.swans.route.metric.RouteMetricInterface.NoLinkExistsException;


class _proactive {
  public interface ProactiveInterface {
    /**
     * RouteExORFlood: sends out a new flooding packet
     */
    void runFloodingSendHook();

    public static class Dlg implements ProactiveInterface, NetInterface.NetHandler,
    Protocol, JistAPI.Proxiable {
      private ProactiveInterface dlg;
      private NetHandler handler;
      private Protocol protocol;

      public Dlg(ProactiveInterface dlg, NetInterface.NetHandler handler,
                 Protocol protocol) {
        this.dlg = dlg;
        this.handler = handler;
        this.protocol = protocol;
      }

      public ProactiveInterface getProxy() {
        return (ProactiveInterface) JistAPI.proxyMany(this, new Class[] {
            ProactiveInterface.class, NetInterface.NetHandler.class, Protocol.class
        });
      }

      /**
       *
       * @see brn.swans.route._proactive.ProactiveInterface#runFloodingSendHook()
       */
      public void runFloodingSendHook() {
        dlg.runFloodingSendHook();
      }

      /**
       * @param msg
       * @param src
       * @param lastHop
       * @param macId
       * @param dst
       * @param priority
       * @param ttl
       * @param anno
       * @see jist.swans.net.NetInterface.NetHandler#receive(jist.swans.misc.Message, jist.swans.net.NetAddress, jist.swans.mac.MacAddress, byte, jist.swans.net.NetAddress, byte, byte, jist.swans.misc.MessageAnno)
       */
      public void receive(Message msg, NetAddress src, MacAddress lastHop, byte macId, NetAddress dst, byte priority, byte ttl, MessageAnno anno) {
        handler.receive(msg, src, lastHop, macId, dst, priority, ttl, anno);
      }

      /**
       *
       * @see jist.swans.misc.Protocol#start()
       */
      public void start() {
        protocol.start();
      }
    }
  }
}

public class RouteDsrDiscoveryProactive extends RouteDsrDiscovery
  implements _proactive.ProactiveInterface, NetInterface.NetHandler, Protocol {

  // ////////////////////////////////////////////////
  // constants
  //

  // (msecs)
  protected static final int DEFER_FORWARD = 20;

  final static int NB_MIN_METRIC = 5000;

  // ////////////////////////////////////////////////
  // locals
  //

  /** logger for BRP events. */
  private static Logger log = Logger.getLogger(RouteDsrDiscoveryProactive.class.getName());


  /** my home channel */
  protected RadioInterface.RFChannel homeChannel;

  /** */
  protected int period; //in msec

  /** */
  protected int offset; //in msec

  /** */
  protected int nextSeqNum;

  /** */
  protected boolean floodingEnabled;

  /** Indicates the maximum number of floodings. */
  protected long maxnofloodings;

  /**
   * list of available channels
   */
  protected List /* int */ channelsInNBhood;

  /**
   * I know the home channel of each node in the network
   */
  protected Hashtable nodeHomeChannels;

  /**
   * Keep track of forwarded flooding packets
   */
  protected Set floodingTable;

  /**
   * The proxy interface for this object.
   */
  protected _proactive.ProactiveInterface self;

  protected AbstractRoute.DiscardEvent discardEvent;

  // ////////////////////////////////////////////////
  // initialization
  //

  /**
   * Create new Flooding Helper Class.
   */
  public RouteDsrDiscoveryProactive(RouteDsrBrn dsr, RouteMetricInterface metric,
      ArpTableInterface arp, NetAddress localAddr, int period, int offset,
      int floodingMax) {
    super(dsr, metric, arp, localAddr);
    this.homeChannel = new RadioInterface.RFChannel(Constants.FREQUENCY_DEFAULT, Constants.CHANNEL_DEFAULT);
    this.period = period;
    this.offset = offset;
    this.nextSeqNum = 0;
    this.floodingEnabled = (floodingMax > 0) ? true : false;
    this.maxnofloodings = floodingMax;

    this.channelsInNBhood = new ArrayList();
    this.nodeHomeChannels = new Hashtable();
    floodingTable = new HashSet();

    this.self = new _proactive.ProactiveInterface.Dlg(this, this, this).getProxy();
  }

  public void setNode(Node node, RouteDsrBrn dsr) {
    this.discardEvent = dsr.getDiscardEvent();

    if (Main.ASSERT)
      Util.assertion(discardEvent != null);
  }

  // ////////////////////////////////////////////////
  // Accessors
  //

  /**
   * Gets the proxy interface for this object.
   *
   * @return the proxy <code>NetInterface.NetHandler</code> interface for this
   *         object
   */
  public NetInterface.NetHandler getProxy() {
    return (NetInterface.NetHandler) self;
  }

  /**
   * Retrieves the protocol id to use with the proactive version.
   */
  public int getNetProtocol() {
    return Constants.NET_PROTOCOL_FLOODING;
  }

  public void setMaxNoFloodings(long maxnofloodings) {
    this.maxnofloodings = maxnofloodings;
  }

  public int getHomeChannel() {
    return homeChannel.getChannel();
  }

  public void setHomeChannel(int homeChannel) {
    this.homeChannel = new RadioInterface.RFChannel(Constants.FREQUENCY_DEFAULT, homeChannel);
  }

  public int getNumberOfChannels() {
    return channelsInNBhood.size();
  }

  public RadioInterface.RFChannel getNodeHomeChannel(NetAddress node) {
    RadioInterface.RFChannel ch = metric.getHomeChannel(node);
    if (ch.equals(RadioInterface.RFChannel.INVALID_RF_CHANNEL))
      return null;
    // legacy support
    return ch;
  }

  public int getPeriod() {
    return period;
  }

  public int getOffset() {
    return offset;
  }

  protected int getNextSeqNum() {
    return nextSeqNum++;
  }

  protected int getJitter(int maxJitter) {
    return Constants.random.nextInt(maxJitter + 1) % (maxJitter + 1);
  }

  protected int deferForward() {
    int maxJitter = DEFER_FORWARD * 500;

    return (DEFER_FORWARD * 1000) + getJitter(maxJitter);
  }


  // ////////////////////////////////////////////////
  // Helper methods
  //

  protected List estimateChannelsInNBhood() {
    // estimate number of channels used by my neighbors
    List neighbors = metric.getNeighbors(localAddr);

    for (int i = 0; i < neighbors.size(); i++) {
      NetAddress nb = (NetAddress) neighbors.get(i);

      // only clear neighbors are considered
      int nbMetric;
      try {
        nbMetric = metric.getLinkMetric(localAddr, nb);
      } catch (NoLinkExistsException e) {
        continue;
      }

      // skip too bad neighbors
      if (nbMetric > NB_MIN_METRIC) {
        //log.debug("calculateCandidateSet() skip nb " + nb + " (MAX_METRIC_FOR_NB exceeded)");
        continue;
      }

      RadioInterface.RFChannel nbHomeChannel = metric.getHomeChannel(nb);
      if (!channelsInNBhood.contains(nbHomeChannel))
        channelsInNBhood.add(nbHomeChannel);
    }

    Collections.sort(channelsInNBhood);
    return channelsInNBhood;
  }

  /**
   * Calculates the time of the next flooding event.
   *
   * @return the time of the next flooding event.
   */
  protected long calcNextSendPacket() {

    if (--maxnofloodings <= 0) {
      log.info(" max number of floodings reached.");
      return Integer.MAX_VALUE;
    }

    // estimate number of channels used by my neighbors
    estimateChannelsInNBhood();

    int channelsInNbhood = getNumberOfChannels();

    // avoid devision by zero
    if (channelsInNbhood == 0) {
      channelsInNbhood = 1;
    }

    int p = period / channelsInNbhood; // period (msecs)
    // count
    int maxJitter = p * 500;

    return (p * 500) + getJitter(maxJitter);
  }

  /**
   * Initiates a flooding request. If a node has not any neighbor this
   * method returns <code>null</code>.
   */
  protected McFloodingMsg initFlooding() {

    if (log.isDebugEnabled()) {
      log.debug(localAddr + "(" + JistAPI.getTime() + "): initFlooding ");
    }

    McFloodingMsg flood = new McFloodingMsg(localAddr, period, getNextSeqNum());

    // estimate my neighbors
    List neighbors = metric.getNeighbors(localAddr);

    for (int i = 0; i < neighbors.size(); i++) {
      NetAddress nb = (NetAddress) neighbors.get(i);

      try {
        int metric = this.metric.getLinkMetric(localAddr, nb);
        LinkEntry entry = new LinkEntry(nb, metric);

        flood.linkEntries.add(entry);
        flood.numLinks++;
      } catch (NoLinkExistsException e) {
        continue;
      }
    }

    // TODO FIXME this statement is useless!!!
    flood.setHomeChannel(homeChannel.getChannel());

    return flood;
  }

  /**
   * Handles an incoming flooding request.
   *
   * @param flood flooding message
   * @param lastHop mac address of the last hop
   * @param prevNode net address of the last hop
   * @param anno flooding message annotations
   */
  protected void handleMsg(McFloodingMsg flood, MacAddress lastHop,
      NetAddress prevNode, MessageAnno anno) {

    // Remember mac address in arp table
    arp.addArpEntry(prevNode, lastHop);

    // sender address (neighbor node)
    NetAddress src = flood.getSrc();

    if (log.isDebugEnabled())
      log.debug(localAddr + "(" + JistAPI.getTime() + "): learning from incoming "
        + "flooding packet: " + src + "/" + flood.getSeqNum());

    // peer node is using the same period, ...
    assert(flood.numLinks == flood.linkEntries.size());
    assert(flood.period == this.period);

    for (int i = 0; i < flood.linkEntries.size(); i++) {
      LinkEntry linkEntry = (LinkEntry) flood.linkEntries.get(i);
      NetAddress nb = linkEntry.ip;
      int metric = linkEntry.metric;

      // update link table
      this.metric.updateBothLinks(src, nb, metric, false);
    }

    log.info(localAddr + "(" + JistAPI.getTime() + "): learning from incoming "
        + "mc flooding packet " + flood.src + " on home ch " + flood.homeChannel);

    // remember the node's home channel
    nodeHomeChannels.put(flood.src, new Integer(flood.homeChannel));
  }

  /**
   * Checks if we have recently seen this flooding packet with the given seqNum
   * coming from the given source.
   * *
   *
   * @param src the source address of the flooding packet
   * @param id  the ID number of the Route Request
   * @return whether the given request has been seen recently.
   */
  protected boolean seenFloodingBefore(NetAddress src, int id) {
    PacketId entry = new PacketId(src, id);
    return floodingTable.contains(entry);
  }

  /**
   * Enters a new Flooding ID number into the Flooding Table.
   *
   * @param src the address of the originator of the flooding packet
   * @param id  the ID number of the Route Request
   */
  protected void addFloodingId(NetAddress src, int id) {
    // Do nothing if it's already in the table
    if (seenFloodingBefore(src, id))
      return;

    // Otherwise add this id to the table
    PacketId entry = new PacketId(src, id);

    floodingTable.add(entry);
  }

  /**
   * Checks whether we need to forward this flooding packet or not.
   *
   * @param floodMsgMc the Flooding packet.
   */
  protected void forwardFlooding(McFloodingMsg floodMsgMc) {
    // Otherwise forward the flooding packet
    if (seenFloodingBefore(floodMsgMc.getSrc(), floodMsgMc.getSeqNum()))
      return;

    // estimate the used channels
    List chInNBhood = estimateChannelsInNBhood();

    // if no neighbors, use home channel
    if (chInNBhood.size() == 0)
      chInNBhood.add(homeChannel);

    for (Iterator it = chInNBhood.iterator(); it.hasNext();) {
      RadioInterface.RFChannel ch = (RadioInterface.RFChannel) it.next();
      int delay = deferForward();
      JistAPI.sleep(Constants.MICRO_SECOND * delay);
      sendFloodingPacket(floodMsgMc, ch);
    }

    // Make a note of this request in the route request table
    addFloodingId(floodMsgMc.getSrc(), floodMsgMc.getSeqNum());
  }

  /**
   * Sends a flooding packet via the given channel.
   *
   * @param msgMc   flooding packet
   * @param ch the channel
   */
  protected void sendFloodingPacket(McFloodingMsg msgMc, RFChannel ch) {

    /** Proactive flooding packets has a higher priority than normal packets ({@link Constants.NET_PRIORITY_D_NETWORKCONTROL}). */
    NetMessage.Ip ipMsg = new NetMessage.Ip(msgMc, localAddr, NetAddress.ANY,
            Constants.NET_PROTOCOL_FLOODING,
            Constants.NET_PRIORITY_D_NETWORKCONTROL, (byte) 1);

    log.info(localAddr + "(" + JistAPI.getTime() + "): forwarding flooding packet: "
            + msgMc.getSrc() + "/" + msgMc.getSeqNum() + " on ch " + ch);

    MessageAnno anno = new MessageAnno();
    //anno.put(MacInterface.ANNO_BITRATE, new Integer(msg.rate));
    //TODO: make the freq dynamic
    anno.put(MessageAnno.ANNO_MAC_RF_CHANNEL, ch);
    anno.put(MessageAnno.ANNO_MAC_FORWARD_PREF, Byte.valueOf(brn.swans.Constants.PREFERENCE_MAX));

    dsr.send(ipMsg, Constants.NET_INTERFACE_DEFAULT, MacAddress.ANY, anno);
  }

  /**
   * Sends a flooding packet.
   *
   * @param msg flooding packet
   */
  protected void sendFloodingPacket(Message msg) {

    NetMessage.Ip ipMsg = new NetMessage.Ip(msg, localAddr, NetAddress.ANY,
            Constants.NET_PROTOCOL_FLOODING,
            Constants.NET_PRIORITY_D_BESTEFFORT, (byte) 1);

    MessageAnno anno = new MessageAnno();
    dsr.send(ipMsg, Constants.NET_INTERFACE_DEFAULT, MacAddress.ANY, anno);
  }

  private void noRouteFoundError(NetAddress dst, NetMessage msg, MessageAnno anno) {
    if (discardEvent.isActive())
      discardEvent.handle(msg, anno);

    log.error(this + "(" + JistAPI.getTime() + ") no route found to " + dst);
    //throw new RuntimeException("No route found to "+dst);
  }


  // ////////////////////////////////////////////////
  // Internal Interface
  //

  /**
   * Sends perdiodically {@link McFloodingMsg} messages. Used by the proactive
   * routing protocol.
   */
  public void runFloodingSendHook() {
    log.info(localAddr + "(" + JistAPI.getTime() + "): runFloodingSendHook; timeout.");

    // TODO cleanup !!
    if (log.isDebugEnabled()) {
          Enumeration knKeys = nodeHomeChannels.keys();

          if (!nodeHomeChannels.isEmpty()) {
            log.debug(localAddr + "(" + JistAPI.getTime() + "): My knowledge before flooding.");
            while (knKeys.hasMoreElements()) {
              NetAddress nodeIp = (NetAddress) knKeys.nextElement();
              Integer nodeChannel = (Integer) nodeHomeChannels.get(nodeIp);
              if (log.isDebugEnabled())
              log.debug(nodeIp + ": " + nodeChannel);
            }
            if (log.isDebugEnabled())
            log.debug("*********");
          } else {
            if (log.isDebugEnabled())
            log.debug(localAddr + "(" + JistAPI.getTime() + "): My knowledge before flooding: EMPTY");
          }
    }

    long delay_nxt = calcNextSendPacket();

    // send probe packet
    McFloodingMsg msg = initFlooding();

    if (msg != null) {
      //forwardFlooding(msg);
      addFloodingId(msg.getSrc(), msg.getSeqNum());

      // estimate the used channels
      List chInNBhood = estimateChannelsInNBhood();

      // if no neighbors, use home channel
      if (chInNBhood.size() == 0)
        chInNBhood.add(homeChannel);

      for (Iterator it = chInNBhood.iterator(); it.hasNext();) {
        RadioInterface.RFChannel ch = (RadioInterface.RFChannel) it.next();
        int delay = deferForward();
        JistAPI.sleep(Constants.MICRO_SECOND * delay);
        sendFloodingPacket(msg, ch);
      }

      //sendFloodingPacket(msg, msg.nextChannel);
    } else {
      // skip this flooding period (we haven't any neighbor)
    }

    // schedule next timeout
    JistAPI.sleep(Constants.MICRO_SECOND * delay_nxt);
    self.runFloodingSendHook();
  }


  // ////////////////////////////////////////////////
  // Discovery Interface
  //

  public void start() {

    /* schedule first flooding */
    if (!floodingEnabled)
      return;

    long delay = offset + getJitter(period);
    // schedule next timeout
    if (log.isDebugEnabled())
      log.debug("start runFloodingSendHook in " + delay + " millis");
    JistAPI.sleep(Constants.MILLI_SECOND * delay);
    self.runFloodingSendHook();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.RouteDsrBrnInterface.Discovery#peek(jist.swans.net.NetMessage, jist.swans.mac.MacAddress, jist.swans.misc.MessageAnno)
   */
  public void peek(NetMessage msg, MacAddress lastHop, MessageAnno anno) {
    NetMessage.Ip ipMsg = (NetMessage.Ip) msg;

    // flooding packet
    if (ipMsg.getPayload() instanceof McFloodingMsg) {
      handleMsg((McFloodingMsg) ipMsg.getPayload(), lastHop, ipMsg.getSrc(), anno);
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.RouteDsrBrnInterface.Discovery#send(jist.swans.net.NetMessage, jist.swans.misc.MessageAnno)
   */
  public void send(NetMessage msg, MessageAnno anno) {
    NetMessage.Ip ipMsg = (NetMessage.Ip) msg;

    // Need to encapsulate and send this packet
    RouteDsrBrnMsg.RouteEntry[] route = getRouteFromLinkTable(ipMsg.getDst());
    if (route == null) {
      noRouteFoundError(ipMsg.getDst(), msg, anno);
      return;
    }

    sendWithRoute(ipMsg, anno, route);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.RouteDsrBrnInterface.Discovery#handleTransmitError(jist.swans.net.NetMessage.Ip, jist.swans.misc.MessageAnno)
   */
  public void handleTransmitError(Ip msg, MessageAnno anno) {
    noRouteFoundError(msg.getDst(), msg, anno);
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.net.NetInterface.NetHandler#receive(jist.swans.misc.Message, jist.swans.net.NetAddress, jist.swans.mac.MacAddress, byte, jist.swans.net.NetAddress, byte, byte, jist.swans.misc.MessageAnno)
   */
  public void receive(Message msg, NetAddress src, MacAddress lastHop, byte macId,
      NetAddress dst, byte priority, byte ttl, MessageAnno anno) {
    forwardFlooding((McFloodingMsg) msg);
  }


  // ////////////////////////////////////////////////
  // Sub-classes
  //

  /**
   * Flooding packet
   */
  public static class McFloodingMsg implements Message {

    /**
     * Net address of node issuing the flooding message.
     */
    protected NetAddress src;

    /**
     * Sequence number of node issuing flooding message.
     */
    protected int seqNum;

    /**
     * period of this node's probe broadcasts, in msecs.
     */
    protected int period;

    /**
     * number of linkEntry entries following
     */
    protected int numLinks;

    protected List /** {@link LinkEntry} */ linkEntries;

    /**
     * Inform all nodes about my home channel.
     */
    protected int homeChannel;

    /**
     * Constructs new Flooding Message object.
     *
     * @param src    net address of this node
     * @param seqNum sequence number of this node
     */
    public McFloodingMsg(NetAddress src, int period, int seqNum) {
      this.src = src;
      this.period = period;
      this.seqNum = seqNum;
      this.linkEntries = new ArrayList();
    }

    /**
     * Constructs new Multi channel Flooding Message object.
     *
     * @param src    net address of this node
     * @param seqNum sequence number of this node
     */
    public McFloodingMsg(NetAddress src, int period, int seqNum, int homeChannel) {
      this(src, period, seqNum);
      this.homeChannel = homeChannel;
    }

    public void setHomeChannel(int homeChannel) {
      this.homeChannel = homeChannel;
    }


    /**
     * Constructs new Multi channel Flooding Message object.
     *
     * @param flood       a non multi channel flooding packet
     * @param homeChannel my home channel
     */
    public McFloodingMsg(McFloodingMsg flood, int homeChannel) {
      this(flood.src, flood.period, flood.seqNum);
      this.linkEntries = flood.linkEntries;
      this.homeChannel = homeChannel;
    }

    /**
     * Return size of packet.
     *
     * @return size of packet
     */
    public int getSize() {

      assert(numLinks == linkEntries.size());

      int byte_size = 4 /* IP src address */
              + /*Integer.SIZE*/32 / 8  /* seqNum */
              + /*Integer.SIZE*/32 / 8  /* period */
              + /*Integer.SIZE*/32 / 8;  /* numLinks */

      byte_size += numLinks * (4 /* IP src address */
              + /*Integer.SIZE*/32 / 8  /* metric */);

      //log.debug("FloodingPacket with size = " + byte_size);

      byte_size += /*Integer.SIZE*/32 / 8; /* homeChannel */

      //log.debug("McFloodingPacket with size = " + byte_size);

      return byte_size;
    }

    public void getBytes(byte[] msg, int offset) {
      int size = getSize();
      for (int i = 0; i < size; i++)
        msg[i + offset] = (byte)0;

      // TODO implement
//      Pickle.InetAddressToArray(src.getIP(), msg, offset + 0);
    }

    /**
     * Returns message ip field.
     *
     * @return message ip field
     */
    public NetAddress getSrc() {
      return src;
    }

    public int getSeqNum() {
      return seqNum;
    }

    public String toString() {
      StringBuffer str = new StringBuffer();
      str.append(", size ").append(getSize());

      str.append(", sender ").append(src);
      str.append(", seqNum ").append(seqNum);

      str.append(", period ").append(period);
      str.append(", numLinks ").append(numLinks);
      for (int i = 0; i < linkEntries.size(); i++)
        str.append(",, numLinksItem ").append(linkEntries.get(i));
      return str.toString();
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final McFloodingMsg that = (McFloodingMsg) o;

      if (homeChannel != that.homeChannel) return false;
      if (numLinks != that.numLinks) return false;
      if (period != that.period) return false;
      if (seqNum != that.seqNum) return false;
      if (linkEntries != null ? !linkEntries.equals(that.linkEntries) : that.linkEntries != null) return false;
      if (src != null ? !src.equals(that.src) : that.src != null) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = (src != null ? src.hashCode() : 0);
      result = 29 * result + seqNum;
      result = 29 * result + period;
      result = 29 * result + numLinks;
      result = 29 * result + (linkEntries != null ? linkEntries.hashCode() : 0);
      result = 29 * result + homeChannel;
      return result;
    }
  }

  /**
   * NB
   */
  protected static class LinkEntry {

    protected NetAddress ip;
    protected int metric;

    public LinkEntry(NetAddress ip, int metric) {
      this.ip = ip;
      this.metric = metric;
    }

    public String toString() {
      StringBuffer str = new StringBuffer();
      str.append("nb ").append(ip);
      str.append("metric ").append(metric);

      return str.toString();
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final LinkEntry linkEntry = (LinkEntry) o;

      if (metric != linkEntry.metric) return false;
      if (ip != null ? !ip.equals(linkEntry.ip) : linkEntry.ip != null) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = (ip != null ? ip.hashCode() : 0);
      result = 29 * result + metric;
      return result;
    }
  }

  private class PacketId {
    protected int id;
    protected NetAddress src;

    public PacketId(NetAddress src, int id) {
      this.src = src;
      this.id = id;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final PacketId that = (PacketId) o;

      if (id != that.id) return false;
      if (!src.equals(that.src)) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = id;
      result = 29 * result + src.hashCode();
      return result;
    }
  }

}
