package brn.swans.route.cs;

import jist.swans.net.NetAddress;
import jist.swans.radio.RadioInterface;
import jist.runtime.Util;
import jist.runtime.JistAPI;
import brn.swans.route.metric.RouteMetricInterface;
import brn.swans.route.metric.RouteMetricInterface.NoLinkExistsException;
import brn.swans.route.metric.RouteMetricInterface.NoRouteExistsException;
import brn.swans.route.NGRouteMcExOR;
import brn.swans.Constants;

import java.util.*;

import org.apache.log4j.Logger;

/**
 * Special candidate set selection for McExOR.
 */
public class McExORCandidateSelection extends CandidateSelection.BestPaths {

  /** McExORCandidateSelection logger. */
  private static Logger log = Logger.getLogger(McExORCandidateSelection.class.getName());
  /** constant for an non-existing link */
  private final static int INVALID_ROUTE_METRIC = 9999;

  public static class NetAddressWithMetricAndRelayPref extends NetAddressWithMetric {

    /* preference to be a relay point */
    public byte pref;

    public NetAddressWithMetricAndRelayPref(NetAddress ip, int metric, int metricToSrc, byte pref) {
      super(ip, metric, metricToSrc);
      this.pref = pref;
    }

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

      final NetAddressWithMetricAndRelayPref that = (NetAddressWithMetricAndRelayPref) o;

      if (pref != that.pref) return false;

      return true;
    }

    public int hashCode() {
      int result = super.hashCode();
      result = 29 * result + (int) pref;
      return result;
    }
  }

  /**
   * Only neighbors with this maximum metric are considered to be potential candidates.
   */
  private static final int MCExOR_CS_MAX_METRIC             = 1000; // 350
  private static final int MCExOR_NB_METRIC_DELTA           = 0; // 100
  private static final int MCExOR_CS_MAX_METRIC_FOR_SAFE_NB = 130; // 115

  private Hashtable /** NetAddress -> {@link NGRouteMcExOR.NodePref} */ relayPreferences;
  /** CrossLayer: MAC makes use of RTS/CTS to detect deaf nodes */
  private boolean useRtsCts;
  private NetAddress excludedNb;
  private RadioInterface.RFChannel rfCh;

  public McExORCandidateSelection(int csSize, NetAddress localAddr,
                                  RouteMetricInterface routeMetric, Hashtable relayPreferences,
                                  boolean useRtsCts) {
    super(csSize, localAddr, routeMetric);
    this.maxMetricForNeighbor = MCExOR_CS_MAX_METRIC;
    this.metricDelta          = MCExOR_NB_METRIC_DELTA;
    this.maxMetricForSafeNB   = MCExOR_CS_MAX_METRIC_FOR_SAFE_NB;
    this.relayPreferences     = relayPreferences;
    this.useRtsCts            = useRtsCts;
  }

  /**
   * This method calculates the candidate set for a packet with source address
   * src and destination dst in regard to the used channels.
   *
   * @param src
   *          the source address of the packet
   * @param dst
   *          the final destination of the packet
   * @param preferHomeChannel
   *          indicates whether we should prefer the use of our home channel
   * @return the list of candidates prioritized my their path metric towards the
   *         final destination as List<NetAddress>
   */
  public List calculate(NetAddress src, NetAddress dst,
      boolean preferHomeChannel, byte[] chArray, NetAddress excludedNb, RadioInterface.RFChannel rfCh) {
    this.excludedNb = excludedNb;
    this.rfCh = rfCh;
    return super.calculate(src, dst, preferHomeChannel, chArray);
  }

  protected List getNeighbors(NetAddress src) {
    List nbs = routeMetric.getNeighbors(src);
    if (excludedNb != null && !nbs.remove(excludedNb))
      throw new RuntimeException("Cannot find node...");
    return nbs;
  }

  /**
   * Overwrite this method. We have to deal with relay preferences.
   */
  protected List calcShortestPathForEachNeighbor(final List neighbors,
      final NetAddress src, final NetAddress dst, final List route, final int metric) {
    List candidates = new ArrayList();
    List backup_cands = new ArrayList();
    RadioInterface.RFChannel myHomeChannel = routeMetric.getHomeChannel(localAddr);

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

      // calculate the metric to this neighbor
      int nbMetric = INVALID_ROUTE_METRIC;
      try {
        nbMetric = routeMetric.getLinkMetric(src, nb);
      } catch (NoLinkExistsException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }

      // skip too bad neighbors
      if (nbMetric > maxMetricForNeighbor)
        continue;

      int routeMetric2 = INVALID_ROUTE_METRIC;
      try {
        List newRoute = routeMetric.queryRoute(nb, dst, maxMetricForNeighbor, maxEtxForNeighbor);

        // calculate path metric
        routeMetric2 = this.routeMetric.getRouteMetric(newRoute,
            INVALID_ROUTE_METRIC, INVALID_ROUTE_METRIC);
      } catch (NoRouteExistsException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      } catch (NoLinkExistsException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }

      RadioInterface.RFChannel nbHomeChannel = this.routeMetric.getHomeChannel(nb);

      if (useRtsCts && rfCh != null && !rfCh.equals(nbHomeChannel)) {
        // we are in the RTC/CTS Crosslayer mode
        continue;
      }

      byte relayPref = Constants.PREFERENCE_MAX;
      if (relayPreferences.get(nb) != null) {
        relayPref = ((NGRouteMcExOR.NodePref) relayPreferences.get(nb)).pref;
        if (log.isDebugEnabled() && relayPref != 0)
          log.debug(localAddr + "(" + JistAPI.getTime() + "): node " + nb + " with relay pref = " + relayPref);
      }

      // prefer always candidates with the same channel
      if (preferHomeChannel && (myHomeChannel.equals(nbHomeChannel))) {
        // candidates on the homechannel are always prefered
      } else if (routeMetric2 == -1 || routeMetric2 + metricDelta > metric) {
        /** may be we end up with not enough candidates. due to our multi-channel feature deafness can occur.
         * so we collect some additional nodes. */
        if (routeMetric2 <= metric + 100)
          backup_cands.add(new NetAddressWithMetricAndRelayPref(nb, routeMetric2, nbMetric, relayPref));
        continue;
      }

      // add potential neighbor node
      candidates.add(new NetAddressWithMetricAndRelayPref(nb, routeMetric2, nbMetric, relayPref));
    }

    // this may happen on the last hop
    if (candidates.size() <= cSetSize - 2 && backup_cands.size() > 0) {
      Collections.sort(backup_cands);
      // more than 2 cands in the set are failing, use some cands from the backup
      candidates.add(backup_cands.get(0));
    }

    return candidates;
  }

  /**
   * Overwrite this method. We have to deal with relay preferences.
   */
  protected NetAddressWithMetric getSafeNeighbor(NetAddress src, List candidates) {
    //
    // find safe nb for each cs
    //
    int metricToBestNb        = INVALID_ROUTE_METRIC;
    int routeMetricOfBestNB   = Integer.MAX_VALUE;
    byte relayPrefBestNB      = Constants.PREFERENCE_MIN;
    NetAddressWithMetric safeNb = null, safeNbBck = null;

    // calculate for each candidate set the safe neighbor
    for (int i = 0; i < candidates.size(); i++) {
      NetAddressWithMetric netAddrWithMetric = (NetAddressWithMetric) candidates.get(i);
      NetAddress nb = netAddrWithMetric.ip;

      int routeMetric = netAddrWithMetric.metric;
      int nbMetric = netAddrWithMetric.metricToSrc;

      byte relayPref = Constants.PREFERENCE_MAX;
      if (relayPreferences.get(nb) != null)
        relayPref = ((NGRouteMcExOR.NodePref) relayPreferences.get(nb)).pref;

      // remember safe candidate per channel
      if (routeMetric < routeMetricOfBestNB
          && nbMetric <= maxMetricForSafeNB
          && (relayPref == Constants.PREFERENCE_MAX /*>= relayPrefBestNB*/)) {
        routeMetricOfBestNB = routeMetric;
        safeNb = netAddrWithMetric;
        relayPrefBestNB = relayPref;
      }

      if (nbMetric < metricToBestNb) {
        metricToBestNb = nbMetric;
        safeNbBck = netAddrWithMetric;
      }

      // backup algorithm
      if (safeNb == null) {
        safeNb = safeNbBck;
      }
    } // end

    return safeNb;
  }

  /**
   * Overwrite this method. We have to deal with relay preferences.
   * @throws NoLinkExistsException 
   */
  protected RadioInterface.RFChannel chooseMostPromisingChannel(Hashtable csSet, NetAddress src,
                                                                boolean preferHomeChannel, byte[] chArray) throws NoLinkExistsException {

    RadioInterface.RFChannel bestChannel = null;

    int currMinValue      = Integer.MAX_VALUE;
    int currMinPref       = Integer.MAX_VALUE;
    Enumeration chKeys    = csSet.keys();
    RadioInterface.RFChannel myHomeChannel = routeMetric.getHomeChannel();
    Util.assertion(!myHomeChannel.equals(RadioInterface.RFChannel.INVALID_RF_CHANNEL));

    //
    // check the candidate set for each channel
    //
    while (chKeys.hasMoreElements()) {
      RadioInterface.RFChannel chKey = (RadioInterface.RFChannel) chKeys.nextElement();

      assert (!chKey.equals(RadioInterface.RFChannel.INVALID_RF_CHANNEL));

      List cs = (List) csSet.get(chKey);

      double pdiff = 1;
      double csCum = 0;
      byte cumPref = 0;

      boolean isSafeLastHop = false;
      for (int i = 0; i < cs.size(); i++) {
        // log.fatal(cs.get(i));
        // calculate link metric to my neighbors
        NetAddressWithMetricAndRelayPref nbInfo = (NetAddressWithMetricAndRelayPref) cs.get(i);

        // cumulate relay preference
        cumPref += nbInfo.pref;

        csCum += nbInfo.metric;

        int nbMetric = routeMetric.getLinkMetric(src, nbInfo.ip);

        if ((nbInfo.metric == 0) && (nbMetric <= maxMetricForSafeNB))
          isSafeLastHop = true;

        double np = 1 - (100.0 / (double) nbMetric); // 1-p
        pdiff *= np;
      }

      // normiere csCum
      csCum /= cs.size();

      if (isSafeLastHop)
        csCum = 0;

      cumPref /= cs.size();

      double pcs = 1 - pdiff; // wahrscheinlichkeit, dass das paket den cset erreicht.

      //
      // algorithm to enforce channel switching
      //
      pcs = enforceChannelSwitch(chKey, chArray, pcs,
          routeMetric.getChannels().length);

      csCum = (1 / pcs) * csCum;

      // prefer candidates with the same channel
      if (preferHomeChannel && (myHomeChannel.equals(chKey))) {
        if (pcs < 0.3)
          log.info(this + "(" + JistAPI.getTime() + "): the prefered cs is quite bad; maybe the network is not dense");
        return chKey;
      }

      /** this candidate set has a better relay preference */
      if (cumPref > currMinPref) {
        currMinPref = cumPref;
        currMinValue = (int) csCum;
        bestChannel = chKey;
        continue;
      }

      if (csCum < currMinValue) {
        currMinValue = (int) csCum;
        bestChannel = chKey;

        if (bestChannel.equals(RadioInterface.RFChannel.INVALID_RF_CHANNEL))
          log.error(this + "(" + JistAPI.getTime() + "): aa");
      }
    }
    return bestChannel;
  }
}
