package click.sim.builder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;

import jist.runtime.JistAPI;
import jist.swans.Constants;
import jist.swans.Node;
import jist.swans.field.Fading;
import jist.swans.field.Field;
import jist.swans.field.Mobility;
import jist.swans.field.PathLoss;
import jist.swans.field.Spatial;
import jist.swans.mac.MacAddress;
import jist.swans.mac.MacDumb;
import jist.swans.mac.MacInfo;
import jist.swans.misc.Location;
import jist.swans.misc.Mapper;
import jist.swans.misc.Util;
import jist.swans.net.NetAddress;
import jist.swans.net.PacketLoss;
import jist.swans.radio.RadioFactory;
import jist.swans.radio.RadioInfo;
import jist.swans.radio.RadioNoise;
import jist.swans.radio.RadioNoiseIndep;
import jist.swans.rate.AnnoRate;
import jist.swans.rate.RateControlAlgorithmIF;
import brn.sim.builder.Builder;
import brn.sim.builder.BuilderException;
import brn.sim.builder.MacBuilder;
import brn.sim.builder.NetBuilder;
import brn.sim.data.dump.WiresharkDump;
import click.runtime.ClickAdapter;
import click.runtime.ClickException;
import click.runtime.ClickInterface;
import click.swans.mac.MacClick;
import click.swans.mac.MacClickMessageFactory;
import click.swans.mac.MacDumbClickMessageFactory;
import click.swans.net.AbstractClickRouter;
import click.swans.net.ClickHostRouter;
import click.swans.net.ClickRemoteRouter;
import click.swans.net.ClickRouter;

public interface ClickBuilder {

  public static final String CONNECTOR_WGT = "class=click.runtime.remote.WrapSshConnector2"
    + "\n port=22"
    + "\n user=root"
    + "\n prompt=root@wgt"
    + "\n keyfile=~/.ssh/id_rsa"
    + "\n timeout=9000";

  public static final String CONNECTOR_WRAP = "class=click.runtime.remote.WrapSshConnector2"
    + "\n port=22"
    + "\n user=root"
    + "\n prompt=root@OpenWrt"
    + "\n keyfile=~/.ssh/id_rsa"
    + "\n timeout=6000";


  public static class MacParams extends MacBuilder.M802_11Params
      implements Builder.ParamsWithBuilder {
    private static final long serialVersionUID = 1L;
    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder.ParamsWithBuilder#builderClass()
     */
    public Class builderClass() {
      return ClickBuilder.Mac.class;
    }

    /** whether the mac should behave like a station and check bssid */
    public boolean isStation = false;

    public boolean isStation() {
      return isStation;
    }
    public void setStation(boolean isStation) {
      this.isStation = isStation;
    }
  }

  public static class NetParams extends NetBuilder.IpParams
      implements Builder.ParamsWithBuilder {
    private static final long serialVersionUID = 1L;
    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder.ParamsWithBuilder#builderClass()
     */
    public Class builderClass() {
      return ClickBuilder.Net.class;
    }

    /** click file to execute with the click router */
    public String clickfile = "res/click/meshnode.click";

    /** name of the library to be loaded for click */
    public String clickLibrary = "jistclick";

    /** whether to use the gui for click */
    public boolean clickGui = false;

    public String getClickfile() {
      return clickfile;
    }

    public String getClickLibrary() {
      return clickLibrary;
    }

    public void setClickLibrary(String clickLibrary) {
      this.clickLibrary = clickLibrary;
    }

    public void setClickfile(String clickfile) {
      this.clickfile = clickfile;
    }
    public boolean isClickGui() {
      return clickGui;
    }
    public void setClickGui(boolean clickGui) {
      this.clickGui = clickGui;
    }
  }

  public static class NetRemoteParams extends NetBuilder.IpParams
      implements Builder.ParamsWithBuilder {
    private static final long serialVersionUID = 1L;
    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder.ParamsWithBuilder#builderClass()
     */
    public Class builderClass() {
      return NetRemote.class;
    }

    /** hosts to use (DNS name / IP) */
    public String[] hosts = new String[] {"192.168.3.81"};

    /** list of node positions */
    public String[] nodePositions = null;

    /** connector to use */
    public String connectorProperties = CONNECTOR_WGT;

    /** click file to execute with the click router */
    public String clickScript = "res/click/meshnode.click";

    /** script which is executed at the start or the run (optional) */
    public String startupScript = "res/click/startup-wgt.sh";

    /** script which is executed at the end or the run (optional) */
    public String shutdownScript = "res/click/shutdown.sh";

    /** port to look for the SimDeviceManager */
    public int portClick = 7777;

    /** data bulk port of the SimDeviceManager */
    public int portClickCallback = 7778;

    /** whether click should forward received packets to the simulator */
    public boolean pullRemotePackets = true;

    /** whether to process the pulled packets from remote */
    public boolean processRemotePackets = true;

    /** whether click should forward packets immediately or at the end,
     * NOTE: only valid if {@link #pullRemotePackets} is true
     */
    public boolean collectRemotePacketsAtEnd = false;

    /** format of received packets */
    public int clickPacketEncap = WiresharkDump.FAKE_DLT_EN10MB;

    /** dump received packets from click, append nodeId + '.pcap' (optional) */
    public String dumpFilePrefix = null;//"dump-node-";
    
    /** properties for varialbe expansion in scripts */
    public Properties scriptVariables = new Properties();


    @Override
    public Object clone() throws CloneNotSupportedException {
      super.clone();
      NetRemoteParams ret = (NetRemoteParams) super.clone();
      if (null != hosts) ret.hosts = (String[]) hosts.clone();
      if (null != nodePositions) ret.nodePositions = (String[]) nodePositions.clone();
      return ret;
    }

    public String getClickScript() {
      return clickScript;
    }
    public void setClickScript(String clickScript) {
      this.clickScript = clickScript;
    }
    public String getStartupScript() {
      return startupScript;
    }
    public void setStartupScript(String startupScript) {
      this.startupScript = startupScript;
    }
    public String getShutdownScript() {
      return shutdownScript;
    }
    public void setShutdownScript(String shutdownScript) {
      this.shutdownScript = shutdownScript;
    }
    public String getDumpFilePrefix() {
      return dumpFilePrefix;
    }
    public void setDumpFilePrefix(String dumpFilePrefix) {
      this.dumpFilePrefix = dumpFilePrefix;
    }
    public String[] getHosts() {
      return hosts;
    }
    public void setHosts(String[] hosts) {
      this.hosts = hosts;
    }
    public int getPortClick() {
      return portClick;
    }
    public void setPortClick(int portClick) {
      this.portClick = portClick;
    }
    public int getPortClickCallback() {
      return portClickCallback;
    }
    public void setPortClickCallback(int portClickCallback) {
      this.portClickCallback = portClickCallback;
    }
    public boolean isCollectRemotePacketsAtEnd() {
      return collectRemotePacketsAtEnd;
    }
    public void setCollectRemotePacketsAtEnd(boolean collectRemotePacketsAtEnd) {
      this.collectRemotePacketsAtEnd = collectRemotePacketsAtEnd;
    }
    public int getClickPacketEncap() {
      return clickPacketEncap;
    }
    public void setClickPacketEncap(int clickPacketEncap) {
      this.clickPacketEncap = clickPacketEncap;
    }
    public boolean isPullRemotePackets() {
      return pullRemotePackets;
    }
    public void setPullRemotePackets(boolean pullRemotePackets) {
      this.pullRemotePackets = pullRemotePackets;
    }
    public boolean isProcessRemotePackets() {
      return processRemotePackets;
    }
    public void setProcessRemotePackets(boolean processRemotePackets) {
      this.processRemotePackets = processRemotePackets;
    }
    public String getConnector() {
      return connectorProperties;
    }
    public void setConnector(String connector) {
      this.connectorProperties = connector;
    }
    public String getConnectorProperties() {
      return connectorProperties;
    }
    public void setConnectorProperties(String connectorProperties) {
      this.connectorProperties = connectorProperties;
    }
    public String[] getNodePositions() {
      return nodePositions;
    }
    public void setNodePositions(String[] nodePositions) {
      this.nodePositions = nodePositions;
    }
    public Properties getScriptVariables() {
      return scriptVariables;
    }
    public void setScriptVariables(Properties scriptVariables) {
      this.scriptVariables = scriptVariables;
    }
  }

  public static class HostNetParams extends NetParams
    implements Builder.ParamsWithBuilder {
    private static final long serialVersionUID = 1L;
    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder.ParamsWithBuilder#builderClass()
     */
    public Class builderClass() {
      return ClickBuilder.HostNet.class;
    }

    /** base offset for the wireline mac address */
    public int wireMacBase = 256 * 256;
    /** name of the wireless nic */
    public String wireNicName = "eth0";

    public int getWireMacBase() {
      return wireMacBase;
    }
    public void setWireMacBase(int wireMacBase) {
      this.wireMacBase = wireMacBase;
    }
    public String getWireNicName() {
      return wireNicName;
    }
    public void setWireNicName(String wireNicName) {
      this.wireNicName = wireNicName;
    }

  }

  /**
   * Builder for click mac
   *
   * @author kurth
   */
  public static class Mac extends MacBuilder implements ClickBuilder {

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

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#build(brn.sim.builder.Builder.Params, jist.swans.Node)
     */
    public Object build(brn.sim.builder.Builder.Params params, Node node) throws BuilderException {
      MacParams opts = (MacParams) params;

      // TODO get radio id
      RadioNoise radio = (RadioNoise) node.getRadio(0);
      MacInfo macInfo = MacInfo.create(radio.getRadioInfo().getMacType());
      macInfo.setRetryLimitShort(opts.retryLimitShort);
      macInfo.setRetryLimitLong(opts.retryLimitLong);
      macInfo.setThresholdRts(opts.thresholdRts);

      MacClick macClick = new MacClick(new MacAddress(node.getNodeId()),
          radio.getRadioInfo(), macInfo, opts.isStation);
      macClick.setMsgFactory(new MacClickMessageFactory());
      return macClick;
    }

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

      mac.THRESHOLD_FRAGMENT = opts.thresholdFragment;

      RadioNoise radio = (RadioNoise) mac.getNode().getRadio(opts.radioId);
      AbstractClickRouter net = (AbstractClickRouter) mac.getNode().getNet();

      Builder builder = getProvider().getBuilder(opts.rateSelection);
      Object rateSelection = builder.build(opts.rateSelection, node);
      getProvider().addHookUp(builder, opts.rateSelection, node, rateSelection);

      // TODO name+encap in params
      byte intId = net.addInterface(mac.getProxy(), mac.getAddress(), "ath0", 
          ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA, true);
      mac.setNetEntity(net.getProxy(), intId);

      mac.setRadioEntity(radio.getProxy());
      mac.setUseAnnotations(opts.useAnnos);
      mac.setPromiscuous(opts.macPromisc);
      mac.setStation(opts.isStation);
      radio.setMacEntity(mac.getProxy());
      if (opts.useBitRateAnnos)
        mac.setRateControlAlgorithm(
            new AnnoRate((RateControlAlgorithmIF) rateSelection));
    }
  }


  /**
   * Builder for click router
   *
   * @author kurth
   */
  public static class Net extends NetBuilder implements ClickBuilder {

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return NetParams.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 {
      NetParams opts = (NetParams) params;
      
      // install gui
      if (opts.clickGui)
        ClickAdapter.useGui();

      // initialize shared protocol mapper
      Mapper protMap = new Mapper(opts.protocolMapper);

      // initialize packet loss models
      PacketLoss outLoss = new PacketLoss.Zero();
      PacketLoss inLoss = createLoss(opts);

      NetAddress address = new NetAddress(node.getNodeId());

      ClickRouter click = null;
      try {
        click = new ClickRouter(address, protMap, inLoss, outLoss);
      } catch (ClickException e) {
        throw new BuilderException("Could not create click router", e);
      }

      return click;
    }

    /*
     * (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 {
      NetParams opts = (NetParams) params;
      ClickRouter click = (ClickRouter) entity;
      // TODO
      // net.setPromiscuous(opts.netPromisc);
      Util.assertion(false == opts.netPromisc);

      ClickAdapter.libraryName = opts.clickLibrary;

      MacClick mac = (MacClick) node.getMac(0);
      click.setMsgFactory(mac.getMsgFactory());

      // TODO add real ethernet wireline interface
      MacAddress macAddrEth0 = new MacAddress(node.getNodeId() + 256 * 256 * 256);
      // eth0 hookup
      MacDumb macEth0 = new MacDumb(macAddrEth0, RadioFactory.createRadioInfoFastEthernet());
      click.addInterface(macEth0.getProxy(), macAddrEth0, "eth0",
          ClickInterface.SIMCLICK_PTYPE_ETHER, false);

      // start click
      try {
        click.createClick(opts.clickfile);
      } catch (ClickException e) {
        throw new BuilderException("Could not start click router", e);
      }

      click.getProtocolProxy().start();

      // TODO
      // // Set dumping on/off
      // click.writeHandler("todump_active", "active", opts.dumpClick ? "true" :
      // "false");
    }
  }

  /**
   * Builder for click router
   *
   * @author kurth
   */
  public static class NetRemote extends NetBuilder implements ClickBuilder {

    private Map<NetRemoteParams, Integer> mapHostNumbers =
      new HashMap<NetRemoteParams, Integer>();

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return NetRemoteParams.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 {
      NetRemoteParams opts = (NetRemoteParams) params;

      // initialize shared protocol mapper
      Mapper protMap = new Mapper(opts.protocolMapper);

      // initialize packet loss models
      PacketLoss outLoss = new PacketLoss.Zero();
      PacketLoss inLoss = createLoss(opts);

      // get host
      Integer currHost = mapHostNumbers.get(opts);
      if (null == currHost)
        currHost = 0;
      String host = opts.hosts[currHost];
      mapHostNumbers.put(opts, currHost+1);

      // Build expansion variables
      Properties propVars = new Properties();
      propVars.putAll(opts.scriptVariables);
      propVars.put("$HOST", host);
      propVars.put("$NODEID", Integer.toString(node.getNodeId()));

      // read start script
      String[] startupScript = readScriptLines(opts.startupScript);
      startupScript = expandVariables(startupScript, propVars);

      // read shutdown script
      String[] shutdownScript = readScriptLines(opts.shutdownScript);
      shutdownScript = expandVariables(shutdownScript, propVars);

      Properties connProp = new Properties();
      try {
        connProp.load(
            new ByteArrayInputStream(opts.connectorProperties.getBytes()));
        // TODO only 1.6
//        connProp.load(new StringReader(opts.connectorProperties));
      } catch (IOException e) {
        throw new BuilderException("Error loading connection properties '"+
            opts.connectorProperties+"'", e);
      }

      NetAddress address = new NetAddress(node.getNodeId());
      ClickRemoteRouter click = null;
      try {
        // create instance
        click = new ClickRemoteRouter(address, protMap, inLoss, outLoss);
        
        // execute startup script, the click is startet in parallel on each host, 
        // so we can shorten waiting times
        click.prepareClick(host, connProp, startupScript, shutdownScript);
      } catch (ClickException e) {
        try {
          if (null != click)
            click.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
        throw new BuilderException("Error creating click router", e);
      }
      
      return click;
    }

    /*
     * (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 {
      final NetRemoteParams opts = (NetRemoteParams) params;
      final ClickRemoteRouter click = (ClickRemoteRouter) entity;
      // TODO
      // net.setPromiscuous(opts.netPromisc);
      Util.assertion(false == opts.netPromisc);

//      MacClick mac = (MacClick) node.getMac(0);
//      click.setMsgFactory(mac.getMsgFactory());
      click.setMsgFactory(new MacClickMessageFactory());

//      // TODO add real ethernet wireline interface
//      MacAddress macAddrEth0 = new MacAddress(node.getNodeId() + 256 * 256 * 256);
//      // eth0 hookup
//      MacDumb macEth0 = new MacDumb(macAddrEth0, RadioFactory.createRadioInfoFastEthernet());
//      click.addInterface(macEth0.getProxy(), macAddrEth0, "eth0",
//          ClickAdapter.SIMCLICK_PTYPE_ETHER, false);

      // Build expansion variables
      Properties propVars = new Properties();
      propVars.putAll(opts.scriptVariables);
      propVars.put("$HOST", click.getHost());
      propVars.put("$NODEID", Integer.toString(node.getNodeId()));

      // read click script
      String clickScript = readScript(opts.clickScript);
      clickScript = expandVariables(clickScript, propVars);

      // get dump file name
      String dumpFile = null;
      if (null != opts.dumpFilePrefix) {
        dumpFile = opts.dumpFilePrefix + node.getNodeId();
      }

      // set node position
      Integer currHost = Util.isInArray(opts.hosts, click.getHost());
      if (opts.nodePositions != null && opts.nodePositions.length > currHost) {
        Location loc = Location.parse(opts.nodePositions[currHost]);
        loc = new Location.Location2D(loc.getX()*10, loc.getY()*10);
        click.setNodePosition(loc);
      }

      if (opts.collectRemotePacketsAtEnd)
        throw new BuilderException("not implemented");

      if (!opts.pullRemotePackets && opts.processRemotePackets)
        throw new BuilderException("inconsistent configuration");

      // start click
      try {
        click.createClick(opts.portClick, opts.portClickCallback, clickScript, 
            opts.clickPacketEncap, dumpFile, opts.pullRemotePackets, 
            opts.processRemotePackets, opts.collectRemotePacketsAtEnd);
      } catch (Exception e) {
        try {
          click.close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
        e.printStackTrace();
        throw new BuilderException("Could not connect to click router", e);
      }

      JistAPI.runAt(new Runnable() {
        public void run() {
          try {
            click.close();
          } catch (IOException e) {}
        }
      }, JistAPI.END);
    }

    private String[] readScriptLines(String script) throws BuilderException {
      String[] ret = null;
      if (null != script) {
        try {
          if (script.contains("\n"))
            ret = Util.asLines(script);
          else
            ret = Util.readLines(script);
        } catch (IOException e) {
          throw new BuilderException("Error reading script '"+script+"'", e);
        }
      }
      return ret;
    }

    private String readScript(String script) throws BuilderException {
      String ret = null;
      if (null != script) {
        try {
          if (script.contains("\n"))
            ret = script;
          else
            ret = Util.readFile(script);
        } catch (IOException e) {
          throw new BuilderException("Error reading script '"+script+"'", e);
        }
      }
      return ret;
    }

    /**
     * Expand variables in input (currently supported: $HOST)
     * 
     * @param input
     * @return
     */
    private String[] expandVariables(String[] script, Properties vars) {
      for (int i = 0; i < script.length; i++)
        script[i] = expandVariables(script[i], vars);
      return script;
    }
    
    private String expandVariables(String script, Properties vars) {
      Enumeration enu = vars.propertyNames();
      while (enu.hasMoreElements()) {
        String var = (String) enu.nextElement();
        script = script.replaceAll(
            Matcher.quoteReplacement(var), 
            vars.getProperty(var));
      }
      return script;
    }
  }


  /**
   * Builder for click router
   *
   * @author kurth
   */
  public static class HostNet extends NetBuilder implements ClickBuilder {

    /*
     * (non-Javadoc)
     * @see brn.sim.builder.Builder#getParamClass()
     */
    public Class getParamClass() {
      return HostNetParams.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 {
      HostNetParams opts = (HostNetParams) params;

      // initialize shared protocol mapper
      Mapper protMap = new Mapper(opts.protocolMapper);

      // initialize packet loss models
      PacketLoss outLoss = new PacketLoss.Zero();
      PacketLoss inLoss = createLoss(opts);

      NetAddress address = new NetAddress(node.getNodeId());

      ClickHostRouter click = null;
      try {
        click = new ClickHostRouter(address, protMap, inLoss, outLoss);
      } catch (ClickException e) {
        throw new BuilderException("Could not create click router", e);
      }

      return click;
    }

    /*
     * (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 {
      HostNetParams opts = (HostNetParams) params;
      ClickHostRouter click = (ClickHostRouter) entity;
      // TODO
      // net.setPromiscuous(opts.netPromisc);
      Util.assertion(false == opts.netPromisc);

      ClickAdapter.libraryName = opts.clickLibrary;

      MacClick mac = (MacClick) node.getMac(0);
      click.setMsgFactory(mac.getMsgFactory());

      {
        Location loc = new Location.Location2D(100, 100);
        Field fieldEth0 = new Field(new Spatial.LinearList(loc), new Fading.None(),
            new PathLoss.None(), new Mobility.Static(),
            Constants.PROPAGATION_LIMIT_DEFAULT);
        node.addField(fieldEth0);

        // build ethernet wireline interface
        RadioInfo radioInfo = RadioFactory.createRadioInfoFastEthernet();
        RadioNoise radioEth0 = new RadioNoiseIndep(radioInfo);
        node.addRadio(radioEth0);

        // add real ethernet wireline interface
        MacAddress macAddrEth0 = new MacAddress(node.getNodeId() + opts.wireMacBase);
        MacDumb macEth0 = new MacDumb(macAddrEth0, radioInfo);
        macEth0.setMsgFactory(new MacDumbClickMessageFactory());
        node.addMac(macEth0);

        click.setMsgFactory(macEth0.getMsgFactory());
        byte netId = click.addHostInterface(macEth0.getProxy(), macAddrEth0,
            opts.wireNicName);

        fieldEth0.addRadio(radioInfo.getId(), radioInfo, radioEth0.getProxy(), new Location.Location2D(20,20));
        radioEth0.setFieldEntity(fieldEth0.getProxy());
        radioEth0.setMacEntity(macEth0.getProxy());
        macEth0.setRadioEntity(radioEth0.getProxy());
        macEth0.setNetEntity(click.getProxy(), netId);
      }

      // start click
      try {
        click.createClick(opts.clickfile);
      } catch (ClickException e) {
        throw new BuilderException("Could not start click router", e);
      }

      click.getProtocolProxy().start();

      // TODO
      // // Set dumping on/off
      // click.writeHandler("todump_active", "active", opts.dumpClick ? "true" :
      // "false");
    }
  }
}
