package brn.sim.builder;

import java.util.Hashtable;

import jist.swans.Constants;
import jist.swans.Node;
import jist.swans.mac.AbstractMac;
import jist.swans.misc.Protocol;
import jist.swans.net.AbstractNet;
import jist.swans.net.NetAddress;
import jist.swans.net.NetInterface;
import jist.swans.net.NetIp;
import jist.swans.radio.RadioNoise;
import jist.swans.rate.RateControlAlgorithmIF;
import jist.swans.route.RouteAodv;
import jist.swans.route.RouteDsr;
import jist.swans.route.RouteInterface;
import jist.swans.route.RouteZrp;
import jist.swans.route.RouteZrpBrp;
import jist.swans.route.RouteZrpIarp;
import jist.swans.route.RouteZrpIerp;
import jist.swans.route.RouteZrpNdp;
import jist.swans.route.RouteZrpZdp;
import jist.swans.route.RouteAodv.AodvStats;
import test.rate.RouteDumb;
import brn.sim.PropertiesContributor;
import brn.swans.mac.MacMcExOR;
import brn.swans.mac.MacTxDOR;
import brn.swans.route.NGRouteMcExOR;
import brn.swans.route.RouteDsrBrn;
import brn.swans.route.RouteDsrDiscovery;
import brn.swans.route.RouteDsrDiscoveryProactive;
import brn.swans.route.RouteDsrDiscoveryReactive;
import brn.swans.route.RouteDsrForwardingPassiveAck;
import brn.swans.route.RouteDsrTxDOR;
import brn.swans.route.RouteTable;
import brn.swans.route.RouteDsrBrnInterface.Discovery;
import brn.swans.route.metric.ArpTable;
import brn.swans.route.metric.ArpTableInterface;
import brn.swans.route.metric.RouteEtxEttMetric;
import brn.swans.route.metric.RouteMetricInterface;
import brn.swans.route.metric.LinkStat.Metric;

public abstract class RouteBuilder extends Builder {

  public static class RouteParams extends Builder.Params {
    private static final long serialVersionUID = 1L;

    /** Whether to use message annotations. Needed bc routing sends packets
      * also without trigger by NET and thus without a hint on using annos. */
    public boolean useAnnos = true;
    /** Routing protocol to use. */
    public int protocol = Constants.NET_PROTOCOL_AODV;
    /** Routing protocol options. */
    public String protocolOpts = "";
    
    public boolean isUseAnnos() {
      return useAnnos;
    }
    public void setUseAnnos(boolean useAnnos) {
      this.useAnnos = useAnnos;
    }
    public int getProtocol() {
      return protocol;
    }
    public void setProtocol(int protocol) {
      this.protocol = protocol;
    }
    public String getProtocolOpts() {
      return protocolOpts;
    }
    public void setProtocolOpts(String protocolOpts) {
      this.protocolOpts = protocolOpts;
    }
  }

  public static class TableParams extends RouteParams {
    private static final long serialVersionUID = 1L;

    /** Whether to forward messages */
    public boolean forward = false;
    /** the routing table */
    public String table = "0.0.0.0/0.0.0.0 255.255.255.255 1";

    public String getTable() {
      return table;
    }
    public void setTable(String table) {
      this.table = table;
    }
    public boolean isForward() {
      return forward;
    }
    public void setForward(boolean forward) {
      this.forward = forward;
    }
  }

  public static class DsrParams extends RouteParams {
    private static final long serialVersionUID = 1L;

    //public int protocol = Constants.NET_PROTOCOL_DSR;
  }

  public static class AodvParams extends RouteParams {
    private static final long serialVersionUID = 1L;

    // AODV options
    /** aodv timeout (s) */
    public int aodvTimeout = 30;
    /** aodv hello message interval (s) */
    public int aodvHelloInterval = 30;
    /** whether to collect aodv stats */
    public boolean aodvStats = false;

    public int getAodvHelloInterval() {
      return aodvHelloInterval;
    }
    public void setAodvHelloInterval(int aodvHelloInterval) {
      this.aodvHelloInterval = aodvHelloInterval;
    }
    public int getAodvTimeout() {
      return aodvTimeout;
    }
    public void setAodvTimeout(int aodvTimeout) {
      this.aodvTimeout = aodvTimeout;
    }
    public boolean isAodvStats() {
      return aodvStats;
    }
    public void setAodvStats(boolean aodvStats) {
      this.aodvStats = aodvStats;
    }
  }

  public static class ZrpParams extends RouteParams {
    private static final long serialVersionUID = 1L;

    /** radius for Zrp neighbor */
    public int radius=2;

    public int getRadius() {
      return radius;
    }

    public void setRadius(int radius) {
      this.radius = radius;
    }
  }

  public static class BrnDsrParams extends RouteParams {
    private static final long serialVersionUID = 1L;

    /** whether to use proactive or reactive route discovery */
    public int forwarding = brn.swans.Constants.FORWARDING_PASSIVEACK;
    /** whether to use proactive or reactive route discovery */
    public int discovery = brn.swans.Constants.DISCOVERY_REACTIVE;
    /** interval between two adjacent floodings */
    public int floodingPeriod = 100000 /* ms */;
    /** offset until the first flooding */
    public int floodintOffset = 50000 /* ms */;
    /** number of floodings to proceed */
    public int floodingMax = Integer.MAX_VALUE;
    /** min link metric for routes */
    public int minLinkMetric = RouteDsrDiscovery.MIN_LINK_METRIC_FOR_DSR;
    /** routing metric to use */
    public Builder.Params metric;
    /** whether to annotate the bit rate stored in link table (needs ETT!) */
    public boolean bitrateFromLinkTable = false;

    public int getDiscovery() {
      return discovery;
    }
    public void setDiscovery(int discovery) {
      this.discovery = discovery;
    }
    public int getFloodingMax() {
      return floodingMax;
    }
    public void setFloodingMax(int floodingMax) {
      this.floodingMax = floodingMax;
    }
    public int getFloodingPeriod() {
      return floodingPeriod;
    }
    public void setFloodingPeriod(int floodingPeriod) {
      this.floodingPeriod = floodingPeriod;
    }
    public int getFloodintOffset() {
      return floodintOffset;
    }
    public void setFloodintOffset(int floodintOffset) {
      this.floodintOffset = floodintOffset;
    }
    public int getForwarding() {
      return forwarding;
    }
    public void setForwarding(int forwarding) {
      this.forwarding = forwarding;
    }
    public Builder.Params getMetric() {
      return metric;
    }
    public void setMetric(Builder.Params routeMetric) {
      this.metric = routeMetric;
    }
    public int getMinLinkMetric() {
      return minLinkMetric;
    }
    public void setMinLinkMetric(int minLinkMetric) {
      this.minLinkMetric = minLinkMetric;
    }
    /* (non-Javadoc)
     * @see brn.sim.builder.Builder.Params#clone()
     */
    public Object clone() throws CloneNotSupportedException {
      BrnDsrParams ret = (BrnDsrParams) super.clone();
      if (null != metric) ret.metric = (Params) metric.clone();
      return ret;
    }
    public boolean isBitrateFromLinkTable() {
      return bitrateFromLinkTable;
    }
    public void setBitrateFromLinkTable(boolean bitrateFromLinkTable) {
      this.bitrateFromLinkTable = bitrateFromLinkTable;
    }
  }

  public static class TxDORParams extends BrnDsrParams {
    private static final long serialVersionUID = 1L;

    /** max size of the candidate set */
    public int candidateSetSize = brn.swans.Constants.FORWARDING_DEFAULT_CANDIDATE_SET_SIZE;
//    /** whether to use global view to prevent duplicates (evaluation only!) */
//    public boolean globalForwarderTable = false;
//    /** etx metric delta for candidate selection */
//    public int metricDelta = 0;
    /** whether to use conservative candidate selection */
    public int candidateSelection = brn.swans.Constants.TXDOR_CSSEL_ETT;
    /** candidate cache timeout */
    public long candidateCacheTimeout = 750 * Constants.MILLI_SECOND;
    /** max number of candidtes to calculate and use simulataneously */
    public int noCandSets = 1;
    /** whether to scale the remaining hops in candidate selection, i.e. the
     * following hop is considered more important. This way the the algorithm
     * chooses more reliable candidates in order to make progress.
     */
    public boolean scaleRemainingHops = true;

    public int getCandidateSetSize() {
      return candidateSetSize;
    }
    public void setCandidateSetSize(int candidateSetSize) {
      this.candidateSetSize = candidateSetSize;
    }
//    public boolean isGlobalForwarderTable() {
//      return globalForwarderTable;
//    }
//    public void setGlobalForwarderTable(boolean globalForwarderTable) {
//      this.globalForwarderTable = globalForwarderTable;
//    }
//    public int getMetricDelta() {
//      return metricDelta;
//    }
//    public void setMetricDelta(int metricDelta) {
//      this.metricDelta = metricDelta;
//    }
    public long getCandidateCacheTimeout() {
      return candidateCacheTimeout;
    }
    public void setCandidateCacheTimeout(long candidateCacheTimeout) {
      this.candidateCacheTimeout = candidateCacheTimeout;
    }
    public int getCandidateSelection() {
      return candidateSelection;
    }
    public void setCandidateSelection(int candidateSelection) {
      this.candidateSelection = candidateSelection;
    }
    public int getNoCandSets() {
      return noCandSets;
    }
    public void setNoCandSets(int noCandSets) {
      this.noCandSets = noCandSets;
    }
    public boolean isScaleRemainingHops() {
      return scaleRemainingHops;
    }
    public void setScaleRemainingHops(boolean scaleRemainingHops) {
      this.scaleRemainingHops = scaleRemainingHops;
    }
  }

  public static class MCExORParams extends BrnDsrParams {
    private static final long serialVersionUID = 1L;

    public int candidateSetSize = brn.swans.Constants.FORWARDING_DEFAULT_CANDIDATE_SET_SIZE;
    public boolean testRun = false;
    public Hashtable testRunOpts;
    public Hashtable testRunOpts2;

    public boolean useRtsCts;

    public int getCandidateSetSize() {
      return candidateSetSize;
    }
    public void setCandidateSetSize(int candidateSetSize) {
      this.candidateSetSize = candidateSetSize;
    }
    public boolean isUseRtsCts() {
      return useRtsCts;
    }
    public void setUseRtsCts(boolean useRtsCts) {
      this.useRtsCts = useRtsCts;
    }
    public boolean isTestRun() {
      return testRun;
    }
    public void setTestRun(boolean testRun) {
      this.testRun = testRun;
    }
    public Hashtable getTestRunOpts() {
      return testRunOpts;
    }
    public void setTestRunOpts(Hashtable testRunOpts) {
      this.testRunOpts = testRunOpts;
    }
    public Hashtable getTestRunOpts2() {
      return testRunOpts2;
    }
    public void setTestRunOpts2(Hashtable testRunOpts2) {
      this.testRunOpts2 = testRunOpts2;
    }
    /* (non-Javadoc)
     * @see brn.sim.builder.RouteBuilder.BrnDsrParams#clone()
     */
    public Object clone() throws CloneNotSupportedException {
      MCExORParams ret = (MCExORParams) super.clone();
      if (null != testRunOpts) ret.testRunOpts = (Hashtable) testRunOpts.clone();
      if (null != testRunOpts2) ret.testRunOpts2 = (Hashtable) testRunOpts2.clone();
      return ret;
    }
  }

  public static class Table extends RouteBuilder {

    public Class getParamClass() {
      return TableParams.class;
    }

    public Object build(Params params, Node node) throws BuilderException {
      TableParams opts = (TableParams) params;
      
      AbstractNet net = (AbstractNet) node.getNet();
      AbstractMac mac = node.getMac(0);
      
      ArpTableInterface arp = new ArpTable(net.getAddress(), mac.getAddress());
      RouteTable route = new RouteTable(net.getAddress(), arp, opts.forward);
      route.initTable(opts.table);
      
      return route;
    }

    public void hookUp(Params params, Node node, Object entity)
        throws BuilderException {
      TableParams opts = (TableParams) params;

      RouteTable route = (RouteTable) entity;
      NetIp net = (NetIp) route.getNode().getNet(); // must be netip

      route.setNetEntity(net.getProxy());
      net.setRouting(route.getRouteProxy());
      net.setProtocolHandler(opts.protocol, route.getRouteProxy());
    }
  }
  
  /**
   * DSR (jist/swans impl) builder.
   *
   * @author kurth
   */
  public static class Dsr extends RouteBuilder {

    public Class getParamClass() {
      return DsrParams.class;
    }

    public Object build(Builder.Params params, Node node) throws BuilderException {
      final NetAddress address = new NetAddress(node.getNodeId());
      return new RouteDsr(address);
    }

    public void hookUp(Builder.Params params, Node node, Object entity) throws BuilderException {
      DsrParams opts = (DsrParams) params;
      RouteDsr dsr = (RouteDsr) entity;
      NetIp net = (NetIp) dsr.getNode().getNet(); // must be netip

      dsr.setNetEntity(net.getProxy());
      net.setRouting(dsr.getRouteProxy());
      net.setProtocolHandler(opts.protocol, dsr.getRouteProxy());
    }
  }

  /**
   * AODV (jist/swans impl) builder.
   *
   * @author kurth
   */
  public static class Aodv extends RouteBuilder {

    public Class getParamClass() {
      return AodvParams.class;
    }

    public Object build(Builder.Params params, Node node) throws BuilderException {
      final NetAddress address = new NetAddress(node.getNodeId());
      return new RouteAodv(address);
    }

    public void hookUp(Builder.Params params, Node node, Object entity) throws BuilderException {
      AodvParams opts = (AodvParams) params;
      RouteAodv aodv = (RouteAodv) entity;
      AbstractNet net = (AbstractNet) aodv.getNode().getNet(); // must be netip

      aodv.setUseAnnos(opts.useAnnos);
      aodv.setNetEntity(net.getProxy());
      aodv.getProxy().start();
      net.setRouting(aodv.getRouteProxy());
      net.setProtocolHandler(opts.protocol, aodv.getRouteProxy());

      if (opts.aodvStats) {
        PropertiesContributor contributor = (PropertiesContributor)
          getProvider().getDataManager().getContributor(PropertiesContributor.ID);
        if (null == contributor) {
          contributor = new PropertiesContributor();
          getProvider().getDataManager().add(contributor);
        }

        AodvStats stats = new AodvStats();
        aodv.setStats(stats);
        String[] path = new String[] { "Node " + node.getNodeId(), "Route", "Aodv stats" };
        contributor.addProperites(stats, path);
      }
    }
  }

  /**
   * ZRP (jist/swans impl) builder.
   *
   * @author kurth
   */
  public static class Zrp extends RouteBuilder {

    public Class getParamClass() {
      return ZrpParams.class;
    }

    public Object build(Params params, Node node) throws BuilderException {
      ZrpParams opts = (ZrpParams) params;
      final NetAddress address = new NetAddress(node.getNodeId());
      return new RouteZrp(address, opts.radius);
    }

    public void hookUp(Params params, Node node, Object entity) throws BuilderException {
      ZrpParams opts = (ZrpParams) params;
      RouteZrp zrp = (RouteZrp) entity;
      NetIp net = (NetIp) zrp.getNode().getNet(); // must be netip

      final boolean zdp = true;
      zrp.setNetEntity(net.getProxy());
      zrp.getProxy().start();
      final RouteInterface.Zrp.Iarp iarp = zdp ? (RouteInterface.Zrp.Iarp) new RouteZrpZdp(
          zrp, "inf")
          : (RouteInterface.Zrp.Iarp) new RouteZrpIarp(zrp, "inf");
      zrp.setSubProtocols(new RouteZrpNdp(zrp), iarp, new RouteZrpBrp(zrp),
          new RouteZrpIerp(zrp));
      // zrp.setStats(zrpStats);

      net.setRouting(zrp.getRouteProxy());
      net.setProtocolHandler(opts.protocol, zrp.getRouteProxy());
    }
  }

  public static class BrnDsr extends RouteBuilder {

    public Class getParamClass() {
      return BrnDsrParams.class;
    }

    public Object build(Params opts, Node node) throws BuilderException {
      BrnDsrParams params = (BrnDsrParams) opts;
      NetAddress addr = new NetAddress(node.getNodeId());

      // create metric
      Builder metricBuilder = getProvider().getBuilder(params.metric);
      RouteMetricInterface metric =
        (RouteMetricInterface) metricBuilder.build(params.metric, node);
      ArpTableInterface arp = metric.getArpTable();
      getProvider().addHookUp(metricBuilder, params.metric, node, metric);

      // create forwarding component
      RouteDsrBrn dsr = null;
      switch (params.forwarding) {
      case brn.swans.Constants.FORWARDING_UNICAST:
        dsr = new RouteDsrBrn(addr, arp);
        break;
      case brn.swans.Constants.FORWARDING_PASSIVEACK:
        dsr = new RouteDsrForwardingPassiveAck(addr, arp);
        break;
      case brn.swans.Constants.FORWARDING_TXDOR:
        dsr = new RouteDsrTxDOR(addr, metric, arp, params.minLinkMetric);
        break;
      }
      if (null == dsr)
        throw new BuilderException("Forwarding not supported");

      // create discovery component
      Discovery discovery = null;
      switch (params.discovery) {
      case brn.swans.Constants.DISCOVERY_PROACTIVE:
        discovery = new RouteDsrDiscoveryProactive(dsr,
            metric, arp, addr, params.floodingPeriod, params.floodintOffset,
            params.floodingMax);
        break;
      case brn.swans.Constants.DISCOVERY_REACTIVE:
        discovery = new RouteDsrDiscoveryReactive(dsr, metric, arp, addr);
        break;
      }
      if (null == discovery)
        throw new BuilderException("Discovery not supported");

      discovery.setMinLinkMetric(params.minLinkMetric);
      discovery.setBitrateFromLinkTable(params.bitrateFromLinkTable);
      dsr.setDiscovery(discovery);
      return dsr;
    }

    public void hookUp(Params opts, Node node, Object entity) throws BuilderException {
      BrnDsrParams params = (BrnDsrParams) opts;
      RouteDsrBrn route = (RouteDsrBrn) entity;
      NetIp net = (NetIp) route.getNode().getNet(); // must be netip

      if (route.getDiscovery() instanceof RouteDsrDiscoveryProactive) {
        RouteDsrDiscoveryProactive reac =
          (RouteDsrDiscoveryProactive)route.getDiscovery();

        net.setProtocolHandler(reac.getNetProtocol(), reac.getProxy());
        ((Protocol)reac.getProxy()).start();
      }

      route.setNetEntity(net.getProxy());

      // node entity hookup
      net.setRouting(route.getRouteProxy());
      net.setProtocolHandler(params.protocol, 
          (NetInterface.RawNetHandler)route.getProxy());
    }
  }

  public static class MCExOR extends RouteBuilder {

    public Class getParamClass() {
      return MCExORParams.class;
    }

    public Object build(Params opts, Node node) throws BuilderException {
      MCExORParams params = (MCExORParams) opts;
      NetAddress addr = new NetAddress(node.getNodeId());

      NetIp net = (NetIp)node.getNet();
      if (!(net.getNode().getMac(0) instanceof MacMcExOR))
        throw new BuilderException("Invalid mac layer for MCExOR");

      // create metric
      Builder metricBuilder = getProvider().getBuilder(params.metric);
      RouteMetricInterface metric =
        (RouteMetricInterface) metricBuilder.build(params.metric, node);
      ArpTableInterface arp = metric.getArpTable();
      getProvider().addHookUp(metricBuilder, params, node, metric);

      // create forwarding component
      NGRouteMcExOR forwarding
          = new NGRouteMcExOR(params.candidateSetSize, addr, metric, arp,
          params.useRtsCts, params.testRun,
          (params.testRunOpts != null) ? (Integer[])params.testRunOpts.get(new Integer(node.getNodeId())) : null,
          (params.testRunOpts2 != null) ? ((Integer[])params.testRunOpts2.get(new Integer(node.getNodeId()))) : null);

      // create discovery component
      Discovery discovery = null;
      switch (params.discovery) {
      case brn.swans.Constants.DISCOVERY_PROACTIVE:
        discovery = new RouteDsrDiscoveryProactive(forwarding,
            metric, arp, addr, params.floodingPeriod, params.floodintOffset,
            params.floodingMax);
        break;
      case brn.swans.Constants.DISCOVERY_REACTIVE:
        discovery = new RouteDsrDiscoveryReactive(forwarding, metric, arp, addr);
        break;
      }
      if (null == discovery)
        throw new BuilderException("Discovery not supported");

      forwarding.setDiscovery(discovery);
      discovery.setBitrateFromLinkTable(params.bitrateFromLinkTable);
      return forwarding;
    }

    public void hookUp(Params opts, Node node, Object entity) throws BuilderException {
      MCExORParams params = (MCExORParams) opts;
      NGRouteMcExOR route = (NGRouteMcExOR) entity;
      NetIp net = (NetIp) route.getNode().getNet(); // must be netip

      if (route.getDiscovery() instanceof RouteDsrDiscoveryProactive) {
        RouteDsrDiscoveryProactive reac =
          (RouteDsrDiscoveryProactive)route.getDiscovery();

        net.setProtocolHandler(reac.getNetProtocol(), reac.getProxy());
        ((Protocol)reac.getProxy()).start();
      }

      route.setNetEntity(net.getProxy());

      // node entity hookup
      net.setRouting(route.getRouteProxy());
      net.setProtocolHandler(params.protocol, route.getRouteProxy());
    }
  }

  public static class TxDOR extends BrnDsr {

//    private ForwarderTable globalForwarderTable;

    public Class getParamClass() {
      return TxDORParams.class;
    }

    public Object build(Params opts, Node node) throws BuilderException {
      TxDORParams params = (TxDORParams) opts;

      params.forwarding = brn.swans.Constants.FORWARDING_TXDOR;
      RouteDsrTxDOR dsr = (RouteDsrTxDOR) super.build(opts, node);

      dsr.setCandidateCacheTimeout(params.candidateCacheTimeout);

      return dsr;
    }

    public void hookUp(Params opts, Node node, Object entity) throws BuilderException {
      super.hookUp(opts, node, entity);
      TxDORParams params = (TxDORParams) opts;
      RouteDsrTxDOR dsr = (RouteDsrTxDOR) entity;

      RadioNoise radio = (RadioNoise) node.getRadio(0);
      MacTxDOR mac =  (MacTxDOR) node.getMac(0);
//      ConstantRate rate =  (ConstantRate) mac.getRateControlAlgorithm();
      RateControlAlgorithmIF rate = mac.getRateControlAlgorithm();

      Integer dataRate = Integer.valueOf(rate.getNextDataRate(null, null, null, 0, 0));
      int controlRate = rate.getControlRate(null, dataRate.intValue());
      Hashtable snrThr = radio.getRadioInfo().getBitrateSnrThresholds();
      double deltaSnr = ((Double)(snrThr.get(dataRate))).doubleValue() -
        ((Double)(snrThr.get(new Integer(controlRate)))).doubleValue();

      Metric metricM = null;
      if (params.candidateSelection == brn.swans.Constants.TXDOR_CSSEL_ETT) {
        if (!(dsr.getRouteMetric() instanceof RouteEtxEttMetric))
          throw new BuilderException("invalid candidate selection/routing metric combination");
        RouteEtxEttMetric metric = (RouteEtxEttMetric) dsr.getRouteMetric();
        metricM = (Metric) metric.getLinkStat().getMetric();
      }

      dsr.setCandidateSelection(params.candidateSelection, deltaSnr, metricM,
          dataRate, params.scaleRemainingHops);
      dsr.setNoCandSets(params.noCandSets);
      dsr.setCsSize(params.candidateSetSize);
//      dsr.setMetricDelta(params.metricDelta);

//      if (params.globalForwarderTable) {
//        if (null == globalForwarderTable)
//          globalForwarderTable = new RouteDsrTxDOR.ForwarderTable();
//
//        dsr.setGlobalForwarderTable(globalForwarderTable);
//      }
    }
  }

  public static class Dumb extends RouteBuilder {

    public Class getParamClass() {
      return DumbParams.class;
    }

    public Object build(Builder.Params params, Node node) throws BuilderException {
      final NetAddress address = new NetAddress(node.getNodeId());
      return new RouteDumb(address);
    }

    public void hookUp(Builder.Params params, Node node, Object entity) throws BuilderException {
      DumbParams opts = (DumbParams) params;
      RouteDumb dumb = (RouteDumb) entity;
      NetIp net = (NetIp) dumb.getNode().getNet(); // must be netip

      dumb.setNetEntity(net.getProxy());
      net.setRouting(dumb.getRouteProxy());
      net.setProtocolHandler(opts.protocol, dumb.getRouteProxy());
    }
  }

  public static class DumbParams extends RouteParams {
    private static final long serialVersionUID = 1L;
  }

}
