package click.swans.net;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Properties;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.runtime.Util;
import jist.swans.field.AbstractField;
import jist.swans.mac.MacAddress;
import jist.swans.mac.MacInterface;
import jist.swans.misc.Location;
import jist.swans.misc.Mapper;
import jist.swans.net.NetAddress;
import jist.swans.net.PacketLoss;
import jist.swans.radio.RadioInfo;

import org.apache.log4j.Logger;

import click.runtime.ClickException;
import click.runtime.ClickInterface;
import click.runtime.remote.ClickInterfaceRemoteImpl;
import click.runtime.remote.ControlSocketSelector;
import click.runtime.remote.DumpHandler;
import click.runtime.remote.RemoteConnector;

/**
 * Net entity which connects to a remote click modular router via sockets.
 *
 * TODO set flow and packet id for incoming packets (from remembered packet ids
 * of outgoing packets)
 *
 * @author kurth
 */
public class ClickRemoteRouter extends AbstractClickRouter {

  /**
   * IP logger.
   */
  public static final Logger log = Logger.getLogger(ClickRemoteRouter.class.getName());

  /**
   * Entity for receiving frames from the click runtime within the IO thread.
   *
   * @author kurth
   */
  private class CallbackHandlerDlg
      implements JistAPI.Proxiable, ClickInterface.CallbackHandler {
    /*
     * (non-Javadoc)
     * @see click.runtime.ClickInterface.CallbackHandler#recvFromClick(int, byte[], int)
     */
    public void recvFromClick(int ifid, byte[] data, int type) {
      ClickRemoteRouter.this.recvFromClick(ifid, data, type);
    }
  }

  private class RemoteCollector implements JistAPI.Entity {
    private ClickInterfaceRemoteImpl clickController;

    public RemoteCollector(ClickInterfaceRemoteImpl clickController) {
      super();
      this.clickController = clickController;
    }

    public void collectPackets() {
      if (JistAPI.getTime() < JistAPI.END)
        JistAPI.sleep(JistAPI.END-JistAPI.getTime());
      try {
        if (!clickController.collectPackets()) {
          JistAPI.sleep(0);
          this.collectPackets();
        }
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

  //////////////////////////////////////////////////
  // Locals
  //

  private RemoteConnector connector;

  private String host;

  private String[] startupScript;

  private String[] shutdownScript;

  private String connectorOutput = "";

  private RemoteCollector collector;

//  private String masterName = "_simDeviceMaster";
//
//  private String addInterfaceHandler = "add_interface";

  private String interfaceString = "";

  protected AbstractField.NodeMoveEvent nodeMoveEvent;

  private int cookie;


  //////////////////////////////////////////////////
  // Initialization
  //

  /**
   * Initialize IP implementation with given address and protocol mapping.
   *
   * @param addr local network address
   * @param protocolMap protocol number mapping
   * @param in incoming packet loss model
   * @param out outgoing packet loss model
   * @throws ClickException
   */
  public ClickRemoteRouter(NetAddress addr, Mapper protocolMap, PacketLoss in,
      PacketLoss out) throws ClickException {
    super(addr, protocolMap, in, out);
    this.nodeMoveEvent = new AbstractField.NodeMoveEvent();
  }

  /**
   * @return the host
   */
  public String getHost() {
    return host;
  }

  public void prepareClick(String host, Properties prop,
      String[] startupScript, String[] shutdownScript) throws ClickException {
    this.host = host;
    this.startupScript = startupScript;
    this.shutdownScript = shutdownScript;

    // Load connector and execute startup script
    String connectorClass = prop.getProperty("class");
    try {
      connector = (RemoteConnector) Class.forName(connectorClass).newInstance();
    } catch (Exception e) {
      throw new ClickException("Unable to create connector from class '"+
          connectorClass+"'", e);
    }
    connector.init(prop);

    String[] script = new String[startupScript.length+shutdownScript.length];
    System.arraycopy(this.shutdownScript, 0, script, 0, this.shutdownScript.length);
    System.arraycopy(this.startupScript, 0, script, this.shutdownScript.length, this.startupScript.length);
    
    try {
      cookie = beginExecuteScript(script, connector);
    } catch (IOException e) {
      throw new ClickException(e.getMessage() + " ("+host+
          " connector output:\n"+connectorOutput+")",e);
    }
  }

  /**
   * Create native click modular router instance
   */
  public void createClick(int portClick, int portClickCallback,
      String clickScript, int encap, String dumpFile, boolean pullRemotePackets,
      boolean processRemotePackets, boolean collectRemotePacketsAtEnd)
          throws ClickException, IOException {
    try {
      finishExecuteScript(cookie, connector);
    } catch (IOException e) {
      throw new ClickException(e.getMessage() + " ("+host+
          " connector output:\n"+connectorOutput+")",e);
    }

    if (log.isInfoEnabled())
      log.info(this.connectorOutput);

    ControlSocketSelector.start();

    final ClickInterfaceRemoteImpl clickController
        = new ClickInterfaceRemoteImpl(new CallbackHandlerDlg(),
            processRemotePackets);
    if (null != dumpFile && dumpFile.length() > 0) {
      clickController.setDumpHandler(new DumpHandler(dumpFile, encap));
    }

    registerInterface(ClickInterface.IFID_KERNELTAP, "local", localAddr, MacAddress.NULL);
    registerInterface(ClickInterface.IFID_KERNELTAP, "serv", localAddr, MacAddress.NULL);

    try {
      clickController.connect(InetAddress.getByName(host), portClick, portClickCallback);
      clickScript = clickScript.replaceAll("SIMINTERFACES",
          "SIMINTERFACES \"" + interfaceString + "\"");
      clickController.configureRouter(clickScript, pullRemotePackets, collectRemotePacketsAtEnd);
    } catch (Exception e) {
      throw new ClickException(e.getMessage() + " (connector output:\n"+
          connectorOutput+")",e);
    }

    // schedule collection of packets
    if (collectRemotePacketsAtEnd) {
      collector = new RemoteCollector(clickController);
      collector.collectPackets();
    }

    this.clickInterface = clickController;
  }

  private void registerInterface(int ifId, String ifName, NetAddress ip, MacAddress mac) {
    // must be registed before the click script is loaded
    if (Main.ASSERT)
      Util.assertion(null == this.clickInterface);

    interfaceString += ifId + " " + ifName + " " + ip.toString() + " "
        + mac.toString() + " ";

//    String interfaceData = ifId + " " + ifName + " " + ip.toString() + " " + mac.toString();
//    clickController.write(masterName , addInterfaceHandler, interfaceData);
  }

  private void executeScript(String[] script, RemoteConnector connector) throws IOException {
    if (null == script || 0 == script.length)
      return;

    connector.connect(host);
    for (int i = 0; i < script.length; i++) {
      connectorOutput += connector.execute(script[i]);
    }
    connector.disconnect();
  }

  private int beginExecuteScript(String[] script, RemoteConnector connector) throws IOException {
    if (null == script || 0 == script.length)
      return -1;

    connector.connect(host);
    return connector.beginExecute(script);
  }

  private void finishExecuteScript(int cookie, RemoteConnector connector) throws IOException {
    if (cookie < 0)
      return;
    connectorOutput += connector.finishExecute(cookie);
    connector.disconnect();
  }

  public void setNodePosition(Location loc) {
    RadioInfo info = null;
    if (nodeMoveEvent.isActive())
      nodeMoveEvent.handle(getNode().getNodeId(), info, loc);
  }

  //////////////////////////////////////////////////
  // overwrites
  //

  /* (non-Javadoc)
   * @see click.swans.net.AbstractClickRouter#close()
   */
  @Override
  public void close() throws IOException {
    IOException e = null;
    try {
      super.close();
    } catch (IOException e1) {
      e = e1;
    }
    
    // sleepBlock makes this section parallel among all nodes
    JistAPI.sleepBlock(0);

    ControlSocketSelector.releaseInstance();

    // execute shutdown script
    try {
//      executeScript(this.shutdownScript, this.connector);
      int cookie = beginExecuteScript(shutdownScript, connector);
      // sleepBlock makes this section parallel among all nodes
      JistAPI.sleepBlock(0);
      finishExecuteScript(cookie, connector);
    } catch(IOException e1) {
      e1.printStackTrace();
      e = e1;
    }

    if (null != e) {
      log.error("Error during shutdown of " + host, e);
      throw e;
    }
  }

  /*
   * (non-Javadoc)
   * @see click.swans.net.AbstractClickRouter#addInterface(jist.swans.mac.MacInterface, jist.swans.mac.MacAddress, java.lang.String, int, boolean)
   */
  @Override
  public byte addInterface(MacInterface macEntity, MacAddress addr,
      String name, int encap, boolean txfeedback) {
    byte ifId = super.addInterface(macEntity, addr, name, encap, txfeedback);

    try {
      registerInterface(ifId, name, localAddr, addr);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    return ifId;
  }

}
