package brn.swans.route.metric;

import java.util.List;

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.MessageBytes;
import jist.swans.net.NetAddress;
import jist.swans.net.NetInterface;
import jist.swans.net.NetMessage;
import jist.swans.net.NetInterface.NetHandler;
import jist.swans.radio.RadioInterface;

import org.apache.log4j.Logger;

import brn.swans.route.metric.LinkTable.LinkData;

/**
 * Implementation of the etx routing metric.
 *
 * <code>
 * metric = RouteEtxMetric.buildEtxMetric();
 * metric.setNetEntity(net);
 * net.setProtocolHandler(NET_PROTOCOL_LINK_PROBE, metric);
 * metric.start();
 * </code>
 *
 * TODO use lookup table for arp entries
 *
 * @author kurth
 */
public class RouteEtxEttMetric extends AbstractRouteMetric
  implements RouteEtxEttMetricInterface, RouteMetricInterface {

  /**
   * McExOR logger.
   */
  private static Logger log = Logger.getLogger(RouteEtxEttMetric.class.getName());

  /**
   * unmarshalls link probe packets on the fly.
   *
   * @author kurth
   */
  public static class LinkProbeConversionHandler implements NetInterface.NetHandler {
    private NetInterface.NetHandler dlg;
    public LinkProbeConversionHandler(NetHandler dlg) {
      this.dlg = dlg;
    }
    public void receive(Message msg, NetAddress src, MacAddress lastHop,
        byte macId, NetAddress dst, byte priority, byte ttl, MessageAnno anno) {
      if (msg instanceof MessageBytes) {
        MessageBytes byteMsg = (MessageBytes) msg;
        msg = new LinkProbe(byteMsg.getBytes(), byteMsg.getOffset());
      }
      dlg.receive(msg, src, lastHop, macId, dst, priority, ttl, anno);
    }
  }

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

  //public final static int MIN_LINK_METRIC = 100;

  public final static long CLEAN_UP_TIMEOUT = 50000 * Constants.MILLI_SECOND;

  public static final long STALE_TIMEOUT = 30 * Constants.SECOND;


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

  /**
   * The IP address of this node.
   */
  private NetAddress localAddr;

  /**
   * The interface to the network layer.
   */
  private NetInterface netEntity;

  /**
   * Indices of the interfaces to probe on.
   */
  private int[] netInterfaces = new int[] {Constants.NET_INTERFACE_DEFAULT};

  /**
   * The good old linkstat.
   */
  private LinkStat linkStat;

  /**
   * The proxy interface for this object.
   */
  private RouteEtxEttMetricInterface self;

  /** link table querier */
  private LinkTableQuerier querier;

  /** link to the arp table */
  private ArpTableInterface arpTable;

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

  /**
   * Constructs a etx route metric object.
   * @param localMacAddr
   */
  public RouteEtxEttMetric(NetAddress localAddr, MacAddress localMacAddr,
      LinkStat linkstat, ArpTableInterface arpTable, LinkTableQuerier querier) {
    this.localAddr = localAddr;
    this.linkStat = linkstat;
    this.arpTable = arpTable;
    this.querier = querier;

    /*
     * NOTE: we use the artificial dlg class for proxy creation, so we can
     * support implementation inheritance.
     */
    self = (RouteEtxEttMetricInterface) JistAPI.proxy(
        new RouteEtxEttMetricInterface.Dlg(this), RouteEtxEttMetricInterface.class);
  }

  /**
   * Builds an etx routing metric object and all associated objects.
   *
   * @param localAddr
   * @param period
   * @param tau
   * @param probes
   * @param numberOfChannels
   * @param rates
   * @param useFastLinkTable
   * @return an etx routing metric object and all associated objects.
   */
  public static RouteEtxEttMetric buildEtxMetric(Node node, NetAddress localAddr,
      MacAddress localMacAddr, long period, long tau, int[] probes,
      int numberOfChannels, List/*int*/ rates, long frequency,
      ArpTableInterface arpTable, boolean useFastLinkTable, int bitRate, long routeCacheTimeout) {

    AvailableRates rtable = null;
    if (null != rates)
      rtable = new AvailableRates(localAddr, rates);
    LinkTable linkTable;
    if (useFastLinkTable)
      linkTable = new LinkTableFast(tau);
    else
      linkTable = new LinkTableSlow(tau);
    LinkTableQuerier querier = new LinkTableQuerier(linkTable, routeCacheTimeout);
    ETXMetric etxMetric = new ETXMetric(localAddr, linkTable, bitRate);
    LinkStat linkstat = new LinkStat(localAddr, period, tau, etxMetric, probes,
        numberOfChannels, rtable, frequency);
    RouteEtxEttMetric metric = new RouteEtxEttMetric(localAddr, localMacAddr, linkstat, arpTable, querier);
    metric.setNode(node);

    linkTable.setLinkMetricChangedEvent(new LinkMetricChangedEvent(linkTable, node.getNodeId()));
    if (metric.linkMetricCreatedEvent.isActive())
      metric.linkMetricCreatedEvent.handle(linkTable, node.getNodeId());

    return metric;
  }

  /**
   * Builds an etx routing metric object and all associated objects.
   *
   * @param localAddr
   * @param period
   * @param tau
   * @param probes
   * @param numberOfChannels
   * @param routeCacheTimeout
   * @return an etx routing metric object and all associated objects.
   */
  public static RouteEtxEttMetric buildEtxMetric(Node node, NetAddress localAddr,
      MacAddress localMacAddr, long period, long tau, int[] probes,
      int numberOfChannels, long frequency, ArpTableInterface arpTable,
      boolean useFastLinkTable, int bitRate, long routeCacheTimeout) {

    return buildEtxMetric(node, localAddr, localMacAddr, period, tau, probes,
        numberOfChannels, null, frequency, arpTable, useFastLinkTable, bitRate, routeCacheTimeout);
  }

  /**
   * Builds an etx routing metric object and all associated objects.
   *
   * @param localAddr
   * @param period
   * @param tau
   * @param probes
   * @param numberOfChannels
   * @param rates
   * @param routeCacheTimeout
   * @return an etx routing metric object and all associated objects.
   */
  public static RouteEtxEttMetric buildEtxMetric(Node node, NetAddress localAddr,
      MacAddress localMacAddr, long period, long tau,  int[] probes,
      int numberOfChannels, List/*int*/ rates, long frequency, LinkTable linkTable,
      ArpTableInterface arpTable, int bitRate, long routeCacheTimeout) {

    AvailableRates rtable = null;
    if (null != rates)
      rtable = new AvailableRates(localAddr, rates);
    ETXMetric etxMetric = new ETXMetric(localAddr, linkTable, bitRate);
    LinkStat linkstat = new LinkStat(localAddr, period, tau, etxMetric, probes,
        numberOfChannels, rtable, frequency);
    LinkTableQuerier querier = new LinkTableQuerier(linkTable, routeCacheTimeout);
    RouteEtxEttMetric metric = new RouteEtxEttMetric(localAddr, localMacAddr, linkstat, arpTable, querier);
    metric.setNode(node);

    return metric;
  }

  // ////////////////////////////////////////////////
  // entity hookup
  //

  /**
   * Sets the interface to the network layer.
   *
   * @param netEntity the interface to the network layer
   */
  public void setNetEntity(NetInterface netEntity) {
    this.netEntity = netEntity;
  }

  public int[] getNetInterfaces() {
    return netInterfaces;
  }

  public void setNetInterfaces(int[] netInterfaces) {
    this.netInterfaces = netInterfaces;
  }

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

  // ////////////////////////////////////////////////
  // Operations
  //

  /**
   * @return the linktable object
   */
  public LinkTable getLinkTable()  {
    return linkStat.getLinkTable();
  }

  /**
   * Add a fixed route to the cache.
   *
   * @param route the route to add
   * @param minLinkMetric the associated min link metric
   */
  public void addFixedRoute(List route, int minLinkMetric) {
    querier.addFixedRoute(route, minLinkMetric);
  }


  // ////////////////////////////////////////////////
  // Overwrites
  //

  /*
   * (non-Javadoc)
   * @see java.lang.Object#toString()
   */
  public String toString() {
    return localAddr + "\n" + linkStat.getLinkTable().printLinks();
  }

  /*
   * (non-Javadoc)
   * @see java.lang.Object#hashCode()
   */
  public int hashCode() {
    final int PRIME = 31;
    int result = 1;
    result = PRIME * result + ((localAddr == null) ? 0 : localAddr.hashCode());
    return result;
  }

  /*
   * (non-Javadoc)
   * @see java.lang.Object#equals(java.lang.Object)
   */
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    final RouteEtxEttMetric other = (RouteEtxEttMetric) obj;
    if (localAddr == null) {
      if (other.localAddr != null)
        return false;
    } else if (!localAddr.equals(other.localAddr))
      return false;
    return true;
  }


  // ////////////////////////////////////////////////
  // RouteEtxMetric Interface
  //

  public void start() {
    self.cleanUpLinkTable();
    self.cleanUpLinkStat();
    //self.runLinkStatLogTimer();

    long delay = Math.abs(linkStat.getJitter(linkStat.getPeriod() / 1));
    //schedule next timeout
    if (log.isDebugEnabled())
      log.debug("start runLinkStatSendHook in " + delay + " millis");
    if (delay <= 0)
      delay = 1;
    JistAPI.sleep(delay);
    self.runLinkStatSendHook();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.etx.RouteEtxMetricInterface#cleanUpLinkTable()
   */
  public void cleanUpLinkTable() {
    if (log.isDebugEnabled())
      log.debug("cleanUpLinkTable; timeout at " + JistAPI.getTimeString());

    // remove old entries from link table
    this.linkStat.getLinkTable().clearStale();

    //schedule next timeout
    JistAPI.sleep(CLEAN_UP_TIMEOUT);
    self.cleanUpLinkTable();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.etx.RouteEtxMetricInterface#cleanUpLinkStat()
   */
  public void cleanUpLinkStat() {
    // remove old entries from linkstat
    linkStat.clearStale();

    //schedule next timeout
    JistAPI.sleep(Constants.MILLI_SECOND * LinkStat.CLEAN_UP_TIMEOUT);
    self.cleanUpLinkStat();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.etx.RouteEtxMetricInterface#runLinkStatSendHook()
   */
  public void runLinkStatSendHook() {
    long delay = linkStat.calcNextSendPacket();

    Util.assertion(delay > 0);

    // send probe packet
    LinkProbe msg = linkStat.sendProbe();

    MessageAnno anno = new MessageAnno();
    anno.put(MessageAnno.ANNO_MAC_BITRATE, msg.getRate());
    anno.put(MessageAnno.ANNO_MAC_RF_CHANNEL, msg.getNextChannel());
//    anno.put(MessageAnno.ANNO_MAC_FORWARD_PREF, new Byte(brn.swans.Constants.PREFERENCE_MAX));

    sendLinkProbePacket(msg, anno);

    //schedule next timeout
    Util.assertion(delay > 0);
    JistAPI.sleep(delay);
    self.runLinkStatSendHook();
  }

  /**
   * Sends a link probe packet
   *
   * @param msg
   */
  private void sendLinkProbePacket(Message msg, MessageAnno anno) {

    // Construct an IP packet with {@link Constants.NET_PRIORITY_D_CONTROLLEDLOAD} priority.
    NetMessage.Ip ipMsg = new NetMessage.Ip(msg, localAddr, NetAddress.ANY,
        Constants.NET_PROTOCOL_LINK_PROBE, Constants.NET_PRIORITY_D_CONTROLLEDLOAD, (byte) 1);

    for (int i = 0; i < netInterfaces.length; i++)
      netEntity.send(ipMsg, netInterfaces[i], MacAddress.ANY, anno);
  }


  // ////////////////////////////////////////////////
  // NetHandler Interface
  //

  public void receive(Message msg, NetAddress src, MacAddress lastHop,
                      byte macId, NetAddress dst, byte priority, byte ttl, MessageAnno anno) {
    if (Main.ASSERT)
      Util.assertion(msg instanceof LinkProbe);

    if (linkProbeArrivedEvent.isActive())
      linkProbeArrivedEvent.handle(msg, anno, lastHop);

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

    linkStat.handleMsg(msg, anno);
  }


  // ////////////////////////////////////////////////
  // RouteMetric Interface
  //

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getRouteMetric(java.util.List, int)
   */
  public int getRouteMetric(List route, int maxLinkMetric, int maxLinkEtx)
      throws NoRouteExistsException, NoLinkExistsException {
    return querier.getRouteMetric(route, maxLinkMetric, maxLinkEtx);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#queryRoute(jist.swans.net.NetAddress, jist.swans.net.NetAddress)
   */
  public List queryRoute(NetAddress addrSrc, NetAddress addrDst,
      int maxLinkMetric, int maxLinkEtx) throws NoRouteExistsException {
    return querier.queryRoute(addrSrc, addrDst, maxLinkMetric, maxLinkEtx, true);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#queryRoute(jist.swans.net.NetAddress, jist.swans.net.NetAddress, int, boolean)
   */
  public List queryRoute(NetAddress addrSrc, NetAddress addrDst,
      int maxLinkMetric, int maxLinkEtx, boolean fromCache)
      throws NoRouteExistsException {
    return querier.queryRoute(addrSrc, addrDst, maxLinkMetric, maxLinkEtx, fromCache);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#updateBothLinks(java.lang.Object, java.lang.Object, int, boolean)
   */
  public boolean updateBothLinks(Object a, Object b, int metric, boolean permanent) {
    // TODO set etx
    LinkData link = linkStat.getLinkTable().getLink(a, b);
    link.update(0, 0, metric, 100);

    // TODO set etx
    link = linkStat.getLinkTable().getLink(b, a);
    link.update(0, 0, metric, 100);

    return true;
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getLinkMetric(jist.swans.net.NetAddress, jist.swans.net.NetAddress)
   */
  public int getLinkMetric(NetAddress addrSrc, NetAddress addrDst) throws NoLinkExistsException {
    return querier.getLinkMetric(addrSrc, addrDst);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#getLink(jist.swans.net.NetAddress, jist.swans.net.NetAddress)
   */
  public LinkData getLink(NetAddress addrSrc, NetAddress addrDst) {
    return getLinkStat().getLinkTable().getLinkInfo(addrSrc, addrDst);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#removeLink(jist.swans.net.NetAddress, jist.swans.net.NetAddress)
   */
  public void removeLink(NetAddress addr1, NetAddress addr2) {
    getLinkStat().getLinkTable().removeLink(addr1, addr2);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getNeighbors(jist.swans.net.NetAddress)
   */
  public List getNeighbors(NetAddress addr) {
    return querier.getNeighbors(addr);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getHomeChannel(jist.swans.net.NetAddress)
   */
  public RadioInterface.RFChannel getHomeChannel(NetAddress nb) {
    return linkStat.getNeighborHomeChannel(nb);
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getHomeChannel()
   */
  public RadioInterface.RFChannel getHomeChannel() {
    return linkStat.getHomeChannel();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#getChannels()
   */
  public RadioInterface.RFChannel[] getChannels() {
    return linkStat.getAdsChannels();
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.hop.RouteMetricInterface#addEventListener(brn.swans.route.metric.hop.RouteMetricInterface.EventListener)
   */
  public void addEventListener(EventListener listener) {
    linkStat.getLinkTable().addEventListener(listener);
  }


  public LinkStat getLinkStat() {
    return linkStat;
  }

  public ArpTableInterface getArpTable() {
    return this.arpTable;
  }

  /**
   * @return the localAddr
   */
  public NetAddress getLocalAddr() {
    return localAddr;
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#getLinkMetricOld(jist.swans.net.NetAddress, jist.swans.net.NetAddress)
   */
  public int getLinkMetricOld(NetAddress prev, NetAddress curr) {
    try {
      return getLinkMetric(prev, curr);
    } catch (NoLinkExistsException e) {
      return 9999;
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#getRouteMetric(java.util.List, int)
   */
  public int getRouteMetric(List route, int invalidRouteMetric) {
    try {
      return getRouteMetric(route, invalidRouteMetric, invalidRouteMetric);
    } catch (NoRouteExistsException e) {
      return 9999;
    } catch (NoLinkExistsException e) {
      return 9999;
    }
  }

  /*
   * (non-Javadoc)
   * @see brn.swans.route.metric.RouteMetricInterface#queryRoute(jist.swans.net.NetAddress, jist.swans.net.NetAddress, int)
   */
  public List queryRoute(NetAddress src, NetAddress dst, int csMinMetric) {
    try {
      return queryRoute(src, dst, csMinMetric, csMinMetric);
    } catch (NoRouteExistsException e) {
      return null;
    }
  }

}
