package click.runtime.remote;

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

import jist.runtime.JistAPI;
import jist.runtime.RealtimeController;
import jist.runtime.ThreadedEntityRef;
import click.runtime.ClickException;
import click.runtime.ClickInterface;

/**
 * Connects to a running click instance on another host via ControlSockets
 *
 * Performance:
 * - Is able to loop more than 4000 packets through click via 127.0.0.1
 * - With local loop on remote host and 100 pps, min turnaround for a 1500 byte
 *   packet is about 2.8 ms, median 3.15 ms, 90. percentile 20 ms
 * - up to 400 pps are supported per WGT in local loop, but with higher turnaround
 * - remote loop (from one node to another over the wireless medium)
 *   - with the curent BRN software, about 300-350 pps are supported, thereafter
 *     the CPU becomes the bottleneck (no WRAP boards)
 *   - this means that only 1&2 Mbps can be saturated (2Mbps ~ 130 pps)
 *
 * @author kurth
 */
public class ClickInterfaceRemoteImpl extends ControlSocket implements ClickInterface {

  /** name of the simdevicemaster in click script (convention) */
  private static final String DEVICE_MASTER_NAME = "_simDeviceMaster";

  /** max. packet size we expect over the wire */
  private static int MAX_PACKET_SIZE = 4096;

  /** whether to execute blocking tx in io thread */
  private static boolean TX_IN_IO_THREAD = false;

  /** channel to the data entpoint on the click router */
  private SocketChannel channel;

  /** our tx buffer */
  private ByteBuffer txBuffer;

  /** tx buffer for IO thread */
  private ByteBuffer txBufferIO;

  /** and our rx buffer */
  private ByteBuffer rxBuffer;

  /** handler for incoming packets */
  private ClickInterface.CallbackHandler callbackHandler;

  /** proxy for the handler, for thread marshaling only */
  private ClickInterface.CallbackHandler proxy;

  /** wireshark dumper (optional) */
  private DumpHandler dumpHandler;

  private boolean processRemotePackets;

  /**
   * Creates a remote click interface impl object.
   *
   * @param callbackHandler handler for incoming packets
   * @param processRemotePackets
   * @param pullRemotePackets
   */
  public ClickInterfaceRemoteImpl(ClickInterface.CallbackHandler callbackHandler,
      boolean processRemotePackets) {
    this.processRemotePackets = processRemotePackets;

    // Create a direct buffer to get bytes from socket.
    // Direct buffers should be long-lived and be reused as much as possible.
    rxBuffer = ByteBuffer.allocate(2*MAX_PACKET_SIZE + 4*Integer.SIZE/8);
    rxBuffer.clear();
    txBuffer = ByteBuffer.allocate(MAX_PACKET_SIZE + 4*Integer.SIZE/8);
    txBuffer.clear();
    txBufferIO = ByteBuffer.allocate(20*MAX_PACKET_SIZE);
    txBufferIO.clear();
    // ensure network byte order
    rxBuffer.order(ByteOrder.BIG_ENDIAN);
    txBuffer.order(ByteOrder.BIG_ENDIAN);
    this.callbackHandler = callbackHandler;
  }

  /**
   * Register dump handller
   *
   * @param dumpHandler
   */
  public void setDumpHandler(DumpHandler dumpHandler) {
    this.dumpHandler = dumpHandler;
  }

  /*
   * (non-Javadoc)
   * @see click.runtime.remote.StatusRemoteAdapter#close()
   */
  public void close() {
    if (null != channel && channel.isOpen()) {
      try {
        // stop the router
        write("","stop","1000000");
      } catch (Exception e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
      }
      try {
        channel.close();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
    channel = null;
    if (null != dumpHandler)
      try {
        dumpHandler.close();
      } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    dumpHandler = null;
  }

  /*
   * (non-Javadoc)
   * @see click.runtime.remote.StatusRemoteAdapter#isConnected()
   */
  public boolean isConnected() {
    if (null == channel)
      return false;
    return super.isConnected() && channel.isConnected();
  }

  /**
   * Connect to remote click instance .
   *
   * @param host
   * @param port
   * @param portCallback
   * @throws IOException
   */
  public void connect(InetAddress host, int port, int portCallback) throws IOException {
    int timeout = 30000 /*ms*/;
    long now = System.currentTimeMillis();
    while (true) {
      try {
        super.connect(host, port);
        break;
      } catch (ConnectException e) {
        if (System.currentTimeMillis() - now > timeout)
          throw e;
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e1) {}
        continue;
      }
    }

    // Create a non-blocking socket channel
    channel = SocketChannel.open();

    // Send a connection request to the server; this method is non-blocking
    channel.connect(new InetSocketAddress(host, portCallback));

    channel.configureBlocking(false);
    ControlSocketSelector.addChannel(channel, this);
  }

  /**
   * Hot-swap the configuration into the router.
   *
   * @param configuration
   * @param pullRemotePackets
   * @param collectRemotePacketsAtEnd
   * @throws ClickException
   * @throws IOException
   */
  public void configureRouter(String configuration, boolean pullRemotePackets,
      boolean collectRemotePacketsAtEnd) throws ClickException, IOException {
    this.write("", "hotconfig", configuration);
    this.write(DEVICE_MASTER_NAME, "pullRemotePackets",
        pullRemotePackets ? "true" : "false");
    this.write(DEVICE_MASTER_NAME, "collectRemotePacketsAtEnd",
        collectRemotePacketsAtEnd ? "true" : "false");
  }

  /**
   * Triggers start of packet collection
   *
   * @return true if finished, false otherwise
   * @throws ClickException
   * @throws IOException
   */
  public boolean collectPackets() throws ClickException, IOException {
    // trigger collection
    this.write(DEVICE_MASTER_NAME, "collectPackets", "true");
    char[] ret = this.read(DEVICE_MASTER_NAME, "collectPackets");

    return String.valueOf(ret).equals("false");
  }

  /*
   * (non-Javadoc)
   * @see click.runtime.StatusInterface#send(int, int, byte[], boolean)
   */
  public void send(int ifid, int type, byte[] data, boolean txfeedback)
      throws ClickException, IOException {
    txBuffer.clear();
    txBuffer.putInt(ifid);
    txBuffer.putInt(type);
    txBuffer.putInt(data.length);
    txBuffer.put(data);
    txBuffer.flip();

    if (!TX_IN_IO_THREAD)
    {
      // poll in simulation thread
      while (txBuffer.hasRemaining())
        channel.write(txBuffer);
    }
    else
    {
      // transfer into IO thread, block if IO queue is full
      synchronized(txBufferIO) {
        while (true) {
          if (txBufferIO.position() == 0) {
            channel.write(txBuffer);
            if (0 >= txBuffer.remaining())
              return;
          }

          // set new select mode and wake up selector
          ControlSocketSelector.addChannel(channel,
              SelectionKey.OP_READ | SelectionKey.OP_WRITE, this);

          try {
            txBufferIO.put(txBuffer);
            break;
          } catch (BufferOverflowException e) {
            try {
              txBufferIO.wait();
            } catch (InterruptedException e1) {
              // TODO Auto-generated catch block
              e1.printStackTrace();
            }
          }
        }
      }

    }
  }

  /**
   * Process incoming data.
   * NOTE: called from within the IO thread, not the sim thread!
   *
   * @param channel the readable channel
   * @throws IOException if you like to...
   */
  void readFromRemote(SocketChannel channel) throws IOException {
    try {
      // Clear the buffer and read bytes from socket
      int numBytesRead = channel.read(rxBuffer);
      while (numBytesRead > 0) { // TODO 0??
        rxBuffer.flip();

        try {
          while (rxBuffer.hasRemaining()) {
            rxBuffer.mark();
            processRequest(rxBuffer);
          }
          rxBuffer.clear();
        } catch (BufferUnderflowException e) {
          // compactify, if still data in buffer, otherwise start anew
          rxBuffer.reset();
          rxBuffer.compact();
        }
        numBytesRead = channel.read(rxBuffer);
      }
    } catch (ClickException e) {
      // Connection may have been closed
      channel.close();
      throw new IOException(e.getMessage());

    } finally {
      if (!channel.isConnected()) {
        channel.close();
        channel = null;
      }

    }
  }

  /**
   * Handle incoming packet.
   * NOTE: called from within the IO thread, not the sim thread!
   * To get the packet into the sim thread, we use a proxy with
   * {@link ThreadedEntityRef}, which does the marshaling.
   *
   * @param buffer
   * @throws ClickException
   */
  private void processRequest(ByteBuffer buffer) throws ClickException {
    int ifid = buffer.getInt();
    int type = buffer.getInt();
    int len = buffer.getInt();

    // TODO check type ...
    if (len <= 0 || len > MAX_PACKET_SIZE)
      throw new ClickException("inconsistend request detected (len="+len+")");

    // ensure that the packet is contained as whole
    if (len > buffer.remaining())
      throw new BufferUnderflowException();

    long time = RealtimeController.getRealTimeStatic();

    byte[] transferBuffer = new byte[len];
    buffer.get(transferBuffer, 0, len);

    // If starting or shutting down, ignore
    if (time <= 0 || time >= JistAPI.END)
      return;

    if (null != dumpHandler)
      dumpHandler.dump(ifid, transferBuffer, type, time);

    if (null == proxy) {
      // Proxy is created here, because we are within a different thread!
      proxy = (ClickInterface.CallbackHandler) RealtimeController.proxy(
          callbackHandler, ClickInterface.CallbackHandler.class);
    }

    // use the proxy to bridge to the sim thread
    if (processRemotePackets)
      proxy.recvFromClick(ifid, transferBuffer, type);
  }

  public void writeToRemote(SocketChannel channel) throws IOException {
    synchronized (txBufferIO) {
      txBufferIO.flip();

      // write if available
      if (txBufferIO.remaining() > 0) {
        channel.write(txBufferIO);
      }
      // remove selector, if no data
      if (0 >= txBufferIO.remaining())
        ControlSocketSelector.getInstance().register(channel,
            SelectionKey.OP_READ, this);

      // restore buffer for jist thread
      txBufferIO.compact();
      txBufferIO.notify();
    }
  }

}
