package brn.sim.builder;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import org.apache.log4j.Logger;

import jist.runtime.Main;
import jist.swans.Constants;
import jist.swans.Node;
import jist.swans.misc.MessageAnno;
import jist.swans.misc.Util;
import jist.swans.net.NetAddress;
import jist.swans.trans.TransUdp;
import jist.swans.trans.TransInterface.TransTcpInterface;
import brn.swans.app.TcpApplication;
import brn.swans.app.UdpApplication;
import brn.swans.app.UdpCaApp;
import brn.swans.net.NetIpNotify;

/**
 * Definition of different flow builders:
 *
 * {@link CbrUdp} is a simple CBR flow of UDP packets
 * {@link SaturatingUdp} sends UDP packets as fast as possible
 * {@link CaUdp} is also a saturating sender, but it uses global knowledge
 * to avoid congestion of the medium
 * {@link Tcp} is a TCP flow
 *
 * @author kurth
 */
public abstract class FlowBuilder extends Builder {

  public static final Logger log = Logger.getLogger(FlowBuilder.class.getName());

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

    /** client node for the stream */
    public NetAddress clientAddr = null;
    /** client port for the stream */
    public int clientPort = 3000;
    /** server node for the stream */
    public NetAddress serverAddr = null;
    /** server port for the stream */
    public int serverPort = 3000;

    /** start of the udp flow in ms */
    public long start = 80000;

    /** id of this flow */
    public int flowId = 0;

    public int getClientPort() {
      return clientPort;
    }
    public void setClientPort(int clientPort) {
      this.clientPort = clientPort;
    }
    public int getServerPort() {
      return serverPort;
    }
    public void setServerPort(int serverPort) {
      this.serverPort = serverPort;
    }
    public long getStart() {
      return start;
    }
    public void setStart(long start) {
      this.start = start;
    }
    public int getFlowId() {
      return flowId;
    }
    public void setFlowId(int flowId) {
      this.flowId = flowId;
    }
    public void deferFlow(int offset) {
      this.start += offset;
    }
    public NetAddress getClientAddr() {
      return clientAddr;
    }
    public void setClientAddr(NetAddress clientAddr) {
      this.clientAddr = clientAddr;
    }
    public NetAddress getServerAddr() {
      return serverAddr;
    }
    public void setServerAddr(NetAddress serverAddr) {
      this.serverAddr = serverAddr;
    }
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result
          + ((clientAddr == null) ? 0 : clientAddr.hashCode());
      result = prime * result + clientPort;
      result = prime * result + flowId;
      result = prime * result
          + ((serverAddr == null) ? 0 : serverAddr.hashCode());
      result = prime * result + serverPort;
      result = prime * result + (int) (start ^ (start >>> 32));
      return result;
    }
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      final FlowParams other = (FlowParams) obj;
      if (clientAddr == null) {
        if (other.clientAddr != null)
          return false;
      } else if (!clientAddr.equals(other.clientAddr))
        return false;
      if (clientPort != other.clientPort)
        return false;
      if (flowId != other.flowId)
        return false;
      if (serverAddr == null) {
        if (other.serverAddr != null)
          return false;
      } else if (!serverAddr.equals(other.serverAddr))
        return false;
      if (serverPort != other.serverPort)
        return false;
      if (start != other.start)
        return false;
      return true;
    }
  }

  public static class TcpParams extends FlowParams {
    private static final long serialVersionUID = 1L;

    /** number of bytes to send through tcp */
    public int tcpBytes = 1460 * 100;

    public int getTcpBytes() {
      return tcpBytes;
    }
    public void setTcpBytes(int tcpBytes) {
      this.tcpBytes = tcpBytes;
    }
    public int hashCode() {
      final int prime = 31;
      int result = super.hashCode();
      result = prime * result + tcpBytes;
      return result;
    }
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (!super.equals(obj))
        return false;
      if (getClass() != obj.getClass())
        return false;
      final TcpParams other = (TcpParams) obj;
      if (tcpBytes != other.tcpBytes)
        return false;
      return true;
    }
  }

  public static class SaturatingUdpParams extends FlowParams {
    private static final long serialVersionUID = 1L;

    /** number of bytes to transmit in a single packet */
    public int udpPayloadLen = 1460;

    /** end of the udp flow in ms */
    public long end = 90000;

    public boolean useAnnos = true;

    /** max number of packets (max = Integer.MAX_VALUE) */
    public int maxNumberOfPackets = Integer.MAX_VALUE;

    public long getEnd() {
      return end;
    }
    public void setEnd(long end) {
      this.end = end;
    }
    public int getFlowId() {
      return flowId;
    }
    public void setFlowId(int flowId) {
      this.flowId = flowId;
    }
    public int getUdpPayloadLen() {
      return udpPayloadLen;
    }
    public void setUdpPayloadLen(int udpPayloadLen) {
      this.udpPayloadLen = udpPayloadLen;
    }
    public boolean isUseAnnos() {
      return useAnnos;
    }
    public void setUseAnnos(boolean useAnnos) {
      this.useAnnos = useAnnos;
    }
    public int getMaxNumberOfPackets() {
      return maxNumberOfPackets;
    }
    public void setMaxNumberOfPackets(int maxNumberOfPackets) {
      this.maxNumberOfPackets = maxNumberOfPackets;
    }
    public int hashCode() {
      final int prime = 31;
      int result = super.hashCode();
      result = prime * result + (int) (end ^ (end >>> 32));
      result = prime * result + maxNumberOfPackets;
      result = prime * result + udpPayloadLen;
      result = prime * result + (useAnnos ? 1231 : 1237);
      return result;
    }
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (!super.equals(obj))
        return false;
      if (getClass() != obj.getClass())
        return false;
      final SaturatingUdpParams other = (SaturatingUdpParams) obj;
      if (end != other.end)
        return false;
      if (maxNumberOfPackets != other.maxNumberOfPackets)
        return false;
      if (udpPayloadLen != other.udpPayloadLen)
        return false;
      if (useAnnos != other.useAnnos)
        return false;
      return true;
    }
    public void deferFlow(int offset) {
      super.deferFlow(offset);
      this.end += offset;
    }
  }

  public static class CbrUdpParams extends SaturatingUdpParams {
    private static final long serialVersionUID = 1L;

    /** transmit rate in packets per sec */
    public double udpTxRate = 30;
    /** whether to use poisson or uniform inter-arrival times */
    public boolean poissonArrival = false;

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = super.hashCode();
      result = prime * result + (poissonArrival ? 1231 : 1237);
      long temp;
      temp = Double.doubleToLongBits(udpTxRate);
      result = prime * result + (int) (temp ^ (temp >>> 32));
      return result;
    }
    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (!super.equals(obj))
        return false;
      if (getClass() != obj.getClass())
        return false;
      CbrUdpParams other = (CbrUdpParams) obj;
      if (poissonArrival != other.poissonArrival)
        return false;
      if (Double.doubleToLongBits(udpTxRate) != Double
          .doubleToLongBits(other.udpTxRate))
        return false;
      return true;
    }
    public boolean isPoissonArrival() {
      return poissonArrival;
    }
    public void setPoissonArrival(boolean poissonArrival) {
      this.poissonArrival = poissonArrival;
    }
    public double getUdpTxRate() {
      return udpTxRate;
    }
    public void setUdpTxRate(double udpTxRate) {
      this.udpTxRate = udpTxRate;
    }
  }

  public static class CaUdpParams extends SaturatingUdpParams {
    private static final long serialVersionUID = 1L;

    public int maxOutstandingPackets = 100;

    public boolean queueLess = false;

    public boolean getQueueLess() {
      return queueLess;
    }

    public void setQueueLess(boolean queueLess) {
      this.queueLess = queueLess;
    }

    public int getMaxOutstandingPackets() {
      return maxOutstandingPackets;
    }
    public void setMaxOutstandingPackets(int maxOutstandingPackets) {
      this.maxOutstandingPackets = maxOutstandingPackets;
    }
    public int hashCode() {
      final int prime = 31;
      int result = super.hashCode();
      result = prime * result + maxOutstandingPackets;
      return result;
    }
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (!super.equals(obj))
        return false;
      if (getClass() != obj.getClass())
        return false;
      final CaUdpParams other = (CaUdpParams) obj;
      if (maxOutstandingPackets != other.maxOutstandingPackets)
        return false;
      return true;
    }
  }

  /**
   * Builds a single tcp flow
   *
   * @author kurth
   */
  public static class Tcp extends FlowBuilder {

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return TcpParams.class;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#build(brn.sim.builder.Builder.Params, jist.swans.Node)
     */
    public Object build(Params params, Node node) throws BuilderException {
      TcpParams opts = (TcpParams) params;
      Util.assertion(opts.clientAddr != null && opts.serverAddr != null);

      TcpApplication tcpApp = null;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        tcpApp = new TcpApplication(opts.start * Constants.MILLI_SECOND,
            opts.serverPort, opts.tcpBytes);
      }
      else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        tcpApp = new TcpApplication(opts.start * Constants.MILLI_SECOND,
            opts.serverAddr, opts.serverPort, opts.tcpBytes, opts.flowId, opts.clientAddr);
      }

      return tcpApp;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#hookUp(brn.sim.builder.Builder.Params, jist.swans.Node, java.lang.Object)
     */
    public void hookUp(Params params, Node node, Object entity) throws BuilderException {
      TcpParams opts = (TcpParams) params;
      TcpApplication app = (TcpApplication) entity;

      NetAddress addr;
      int port;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        addr = opts.clientAddr;
        port = opts.clientPort;
      } else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        addr = opts.serverAddr;
        port = opts.serverPort;
      } else
        return;
//      node.getTransport(0).setFlow(opts.flowId, addr, port);

      app.setTcpEntity((TransTcpInterface)node.getTransport(0).getTransProxy());
      app.getProxy().run();
    }
  }

  /**
   * Builds a single udp flow
   *
   * @author kurth
   */
  public static class CbrUdp extends FlowBuilder {

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return CbrUdpParams.class;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#build(brn.sim.builder.Builder.Params, jist.swans.Node)
     */
    public Object build(Params params, Node node) throws BuilderException {
      CbrUdpParams opts = (CbrUdpParams) params;
      Util.assertion(opts.clientAddr != null && opts.serverAddr != null);

      UdpApplication udpApp = null;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        udpApp = new UdpApplication(node.getNet().getAddress(), opts.serverPort,
            opts.start * Constants.MILLI_SECOND, opts.end * Constants.MILLI_SECOND);
      }
      else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        // create payload
        byte[] payload = new byte[opts.udpPayloadLen];
        Random random = new Random(1);
        random.nextBytes(payload);

        // create anno
        MessageAnno anno = new MessageAnno();
        anno.put(MessageAnno.ANNO_RTG_FLOWID, new Integer(opts.flowId));

        // create client
        udpApp = new UdpApplication(opts.clientAddr, opts.clientPort,
            opts.serverAddr, opts.serverPort, opts.start * Constants.MILLI_SECOND,
            opts.end * Constants.MILLI_SECOND, opts.udpTxRate, payload, anno);
        udpApp.setMaxNumberOfPackets(opts.maxNumberOfPackets);
        udpApp.setPoissonArrival(opts.poissonArrival);
      }

      return udpApp;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#hookUp(brn.sim.builder.Builder.Params, jist.swans.Node, java.lang.Object)
     */
    public void hookUp(Params params, Node node, Object entity) throws BuilderException {
      CbrUdpParams opts = (CbrUdpParams) params;
      UdpApplication app = (UdpApplication) entity;
      // todo node.getTransport(TransUdp);
      TransUdp udp = (TransUdp) node.getTransport(0);

      NetAddress addr;
      int port;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        addr = opts.clientAddr;
        port = opts.clientPort;
      } else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        addr = opts.serverAddr;
        port = opts.serverPort;
      } else
        return;
//      node.getTransport(0).setFlow(opts.flowId, addr, port);

      app.setUseAnnos(opts.useAnnos);
      app.setUdpEntity(udp.getProxy());
      app.getProxy().run();
    }
  }

  public static class SaturatingUdp extends FlowBuilder {

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return SaturatingUdpParams.class;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#build(brn.sim.builder.Builder.Params, jist.swans.Node)
     */
    public Object build(Params params, Node node) throws BuilderException {
      SaturatingUdpParams opts = (SaturatingUdpParams) params;
      Util.assertion(opts.clientAddr != null && opts.serverAddr != null);

      UdpApplication udpApp = null;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        udpApp = new UdpApplication(node.getNet().getAddress(), opts.serverPort,
            opts.start * Constants.MILLI_SECOND, opts.end * Constants.MILLI_SECOND);
      }
      else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        Node clientNode = node;

        // create payload
        byte[] payload = new byte[opts.udpPayloadLen];
        Random random = new Random(1);
        random.nextBytes(payload);

        // create anno
        MessageAnno anno = new MessageAnno();
        anno.put(MessageAnno.ANNO_RTG_FLOWID, new Integer(opts.flowId));

        // create client
        udpApp = new UdpApplication(opts.clientAddr, opts.clientPort,
            opts.serverAddr, opts.serverPort, opts.start * Constants.MILLI_SECOND,
            opts.end * Constants.MILLI_SECOND, payload, anno);

        udpApp.setMaxNumberOfPackets(opts.maxNumberOfPackets);
        // client net must be net notify
        NetIpNotify net = (NetIpNotify) clientNode.getNet();
        net.setNetNotify(udpApp.getNotifyProxy());
      }

      return udpApp;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#hookUp(brn.sim.builder.Builder.Params, jist.swans.Node, java.lang.Object)
     */
    public void hookUp(Params params, Node node, Object entity) throws BuilderException {
      SaturatingUdpParams opts = (SaturatingUdpParams) params;
      UdpApplication app = (UdpApplication) entity;
      TransUdp udp = (TransUdp) node.getTransport(0);

      NetAddress addr;
      int port;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        addr = opts.clientAddr;
        port = opts.clientPort;
      } else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        addr = opts.serverAddr;
        port = opts.serverPort;
      } else
        return;
//      node.getTransport(0).setFlow(opts.flowId, addr, port);

      app.setUseAnnos(opts.useAnnos);
      app.setUdpEntity(udp.getProxy());
      app.getProxy().run();
    }
  }

  public static class CaUdp extends FlowBuilder {

    protected static class Data {
      public UdpCaApp udpClient;
      public UdpCaApp udpServer;
    }

    protected Map dataMap = new HashMap();

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return CaUdpParams.class;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#build(brn.sim.builder.Builder.Params, jist.swans.Node)
     */
    public Object build(Params params, Node node) throws BuilderException {
      CaUdpParams opts = (CaUdpParams) params;
      Util.assertion(opts.clientAddr != null && opts.serverAddr != null);

      Data data = (Data) dataMap.get(opts);
      if (null == data) {
        data = new Data();
        dataMap.put(opts, data);
      }

      UdpCaApp udpApp = null;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        udpApp = new UdpCaApp(node.getNet().getAddress(), opts.serverPort,
            opts.start * Constants.MILLI_SECOND, opts.end * Constants.MILLI_SECOND,
            opts.maxOutstandingPackets, opts.queueLess);
        data.udpClient = udpApp;
      }
      else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        Node clientNode = node;

        // create payload
        byte[] payload = new byte[opts.udpPayloadLen];
        Random random = new Random(1);
        random.nextBytes(payload);

        // create anno
        MessageAnno anno = new MessageAnno();
        anno.put(MessageAnno.ANNO_RTG_FLOWID, new Integer(opts.flowId));

        // create client
        udpApp = new UdpCaApp(opts.clientAddr, opts.clientPort,
            opts.serverAddr, opts.serverPort, opts.start * Constants.MILLI_SECOND,
            opts.end * Constants.MILLI_SECOND, payload, anno,
            opts.maxOutstandingPackets, opts.queueLess);
        data.udpServer = udpApp;

        udpApp.setMaxNumberOfPackets(opts.maxNumberOfPackets);
        // client net must be net notify
        if (clientNode.getNet() instanceof NetIpNotify) {
          NetIpNotify net = (NetIpNotify) clientNode.getNet();
          net.setNetNotify(udpApp.getNotifyProxy());
        }
        else
          log.warn(node + " does not have notify net layer");
      }

      return udpApp;
    }

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#hookUp(brn.sim.builder.Builder.Params, jist.swans.Node, java.lang.Object)
     */
    public void hookUp(Params params, Node node, Object entity) throws BuilderException {
      CaUdpParams opts = (CaUdpParams) params;
      Data data = (Data) dataMap.get(params);
      if (Main.ASSERT)
        Util.assertion(null != data
            && (null != data.udpServer || null != data.udpClient));

      NetAddress addr;
      int port;
      if (node.getNet().getAddress().equals(opts.serverAddr)) {
        addr = opts.clientAddr;
        port = opts.clientPort;
      } else if (node.getNet().getAddress().equals(opts.clientAddr)) {
        addr = opts.serverAddr;
        port = opts.serverPort;
      } else
        return;
//      node.getTransport(0).setFlow(opts.flowId, addr, port);

      UdpCaApp app = (UdpCaApp) entity;
      TransUdp udp = (TransUdp) node.getTransport(0);

      if (null != data.udpServer && null != data.udpClient) {
        if (data.udpServer.equals(app))
          app.setOtherApp(data.udpClient.getUdpCaProxy());
        else
          app.setOtherApp(data.udpServer.getUdpCaProxy());
      }

      app.setUseAnnos(opts.useAnnos);
      app.setUdpEntity(udp.getProxy());
      app.getProxy().run();
    }
  }
}
