package click.swans.net;

import java.io.Closeable;
import java.io.IOException;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.swans.Constants;
import jist.swans.mac.MacAddress;
import jist.swans.mac.MacInterface;
import jist.swans.mac.MacMessage;
import jist.swans.mac.MacMessageFactory;
import jist.swans.mac.MacMessage.HasDst;
import jist.swans.misc.Event;
import jist.swans.misc.Mapper;
import jist.swans.misc.Message;
import jist.swans.misc.MessageAnno;
import jist.swans.misc.Util;
import jist.swans.misc.Message.IllegalFormatException;
import jist.swans.net.MessageQueue;
import jist.swans.net.NetAddress;
import jist.swans.net.NetInterface;
import jist.swans.net.NetIp;
import jist.swans.net.NetMessage;
import jist.swans.net.PacketLoss;
import jist.swans.radio.RadioInterface.RFChannel;
import jist.swans.route.RouteInterface;

import org.apache.log4j.Logger;

import sun.misc.HexDumpEncoder;

import brn.swans.radio.biterrormodels.BitErrorMask;
import brn.swans.radio.biterrormodels.BitErrorMask.BitErrorData;
import click.runtime.ClickException;
import click.runtime.ClickInterface;
import click.swans.mac.MacClickMessage;
import click.swans.mac.MacClickMessageFactory;
import click.swans.mac.MacClickMessage.ExtraHeader;

/**
 *
 * TODO: implement an 802.3 mac/radio/field
 *
 * @author kurth
 *
 */
public abstract class AbstractClickRouter extends NetIp
    implements NetInterface, Closeable {

//  // check if payload is expanded
//  Message payload = ipmsg.getPayload();
//  if (payload instanceof MessageBytes) {
//
//  }

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

  /** called when click is started */
  public static class ClickStartetEvent extends Event {
    private AbstractClickRouter router;

    public ClickStartetEvent(AbstractClickRouter router) {
      this.router = router;
    }

    protected void handle() {
      this.handle(router.getNode().getNodeId());
    }
  }

  /** called when click is shut down TODO */
  public static class ClickShutdownEvent extends Event {
    private AbstractClickRouter router;

    public ClickShutdownEvent(AbstractClickRouter router) {
      this.router = router;
    }

    protected void handle() {
      this.handle(router.getNode().getNodeId());
    }
  }

  /** pulls data from click regularly*/
  public static class ClickPullEvent extends Event {
    private AbstractClickRouter router;
    private long pollingInterval;

    public ClickPullEvent(AbstractClickRouter router, long pollingInterval) {
      this.router = router;
      this.pollingInterval = pollingInterval;
    }

    public void  reschedule(long interval) {
      JistAPI.runAt(new Runnable() {
        public void run() {
          ClickPullEvent.this.handle();
        }
      }, JistAPI.getTime() + interval);
    }

    protected void handle() {
      this.handle(router.getNode().getNodeId());

      // reschedule
      reschedule(pollingInterval);
    }

    public AbstractClickRouter getRouter() {
      return router;
    }
  }

  protected ClickStartetEvent clickStartedEvent;
  protected ClickShutdownEvent clickShutdownEvent;
  protected ClickPullEvent clickPullEvent;

  /**
   * Information about each network interface.
   */
//  public abstract static class NicInfo {
//    public String name;
//    public MacAddress addr;
//    public int encap;
//    /** mac entity. */
//    public MacInterface mac;
//    /** packet currently transmitted. */
//    public Message packet;
//  }
  public static class ClickNicInfo extends NicInfo {
    public String name;
    public MacAddress addr;
    public int encap;
//    /** mac entity. */
//    public MacInterface mac;
    /** packet currently transmitted. */
    public Message packet;
    public boolean txfeedback;
  }



  //////////////////////////////////////////////////
  // locals
  //

  /** adapter to click */
  protected ClickInterface clickInterface;

  /** whether interface is ready to transmit*/
  protected boolean[] nics_ready;

  /** true, if all ip should be passed up */
  protected boolean promisc;

  /** remember flow and packet ids */
  protected ClickFlowStore flowStore;

  /** our click message factroy */
  private MacClickMessageFactory msgFactory;

  //////////////////////////////////////////////////
  // Initialisation
  //

  /**
   * 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
   */
  public AbstractClickRouter(NetAddress addr, Mapper protocolMap, PacketLoss in,
      PacketLoss out) throws ClickException
  {
    super(addr, protocolMap, in, out);
    if (null == this.nics_ready)
      this.nics_ready = new boolean[0];
    // add loopback mac:
    //   therefore, loopback = 0, Constants.NET_INTERFACE_LOOPBACK
    //              next     = 1, Constants.NET_INTERFACE_DEFAULT

    flowStore = new ClickFlowStore();

    // TODO do we need a loopback??
//    MacLoop loopback = new MacLoop();
//    byte netid = addInterface(loopback.getProxy());
//    if(Main.ASSERT) Util.assertion(netid==Constants.NET_INTERFACE_LOOPBACK);
//    loopback.setNetEntity(getProxy(), netid);
  }

  //////////////////////////////////////////////////
  // entity hookup
  //

  public void setMsgFactory(MacMessageFactory msgFactory) {
    this.msgFactory = (MacClickMessageFactory)msgFactory;
  }

  /*
   * (non-Javadoc)
   * @see java.io.Closeable#close()
   */
  public void close() throws IOException {
    if (null != clickInterface)
      clickInterface.close();
    clickInterface = null;
  }

  public boolean isPromisc() {
    return promisc;
  }

  public void setPromisc(boolean promisc) {
    this.promisc = promisc;
  }

  protected ClickNicInfo getNicInfo(int id) {
    return (ClickNicInfo) this.nics[id];
  }


  //////////////////////////////////////////////////
  // routing, protocols, interfaces
  //

  /*
   * (non-Javadoc)
   * @see jist.swans.net.AbstractNet#addInterface(jist.swans.mac.MacInterface, jist.swans.net.MessageQueue)
   */
  public byte addInterface(MacInterface proxy, MessageQueue queue) {
    MacAddress macAddress = MacAddress.LOOP;
    String name = "lo";
    if (null != node) {
      macAddress = new MacAddress(node.getNodeId());
      name = "ath";
    }
    // node entity hookup
    return addInterface(proxy, macAddress,
        name + nics.length, ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA, true);
  }

  /**
   * Add network interface.
   *
   * @param macEntity link layer entity
   * @return network interface identifier
   */
  public byte addInterface(MacInterface macEntity, MacAddress addr, String name,
      int encap, boolean txfeedback) {
    if(!JistAPI.isEntity(macEntity)) throw new IllegalArgumentException("expected entity");
    // create new nicinfo
    ClickNicInfo ni = new ClickNicInfo();
    ni.mac = macEntity;
    ni.packet = null;
    ni.name = name;
    ni.addr = addr;
    ni.encap = encap;
    ni.txfeedback = txfeedback;
    return addInterface(ni);
  }

  /**
   * Registers the given interface internally.
   *
   * @param ni the interface to register
   * @return the newly assigned interface id.
   */
  protected byte addInterface(NicInfo ni) {
    // store
    NicInfo[] nics2 = new NicInfo[nics.length+1];
    System.arraycopy(nics, 0, nics2, 0, nics.length);
    nics2[nics.length] = ni;
    nics = nics2;
    // clone nics_ready
    boolean[] nics_ready2 = new boolean[nics.length];
    int len = 0;
    if (null != nics_ready) {
      len = nics_ready.length;
      System.arraycopy(nics_ready, 0, nics_ready2, 0, len);
    }
    nics_ready2[len] = true;
    nics_ready = nics_ready2;
    // return interface id
    return (byte)(nics.length-1);
  }

  //////////////////////////////////////////////////
  // NetInterface implementation
  //

  /*
   * (non-Javadoc)
   * @see click.swans.net.AbstractClickRouter#send(jist.swans.net.NetMessage.Ip, int, jist.swans.mac.MacAddress, jist.swans.misc.MessageAnno)
   */
  @Override
  public void send(NetMessage.Ip ip, int interfaceId, MacAddress nextHop,
      MessageAnno anno) {

    // From local JiST routing or other sources (e.g. metric)

    flowStore.put(ip, anno);

    try {
      this.sendToClick(ip, null, ClickInterface.IFID_KERNELTAP,
          ClickInterface.SIMCLICK_PTYPE_IP);
    } catch (ClickException e) {
      throw new RuntimeException(e);
    }
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.net.NetIp#sendIp(jist.swans.net.NetMessage.Ip, jist.swans.misc.MessageAnno)
   */
  @Override
  protected void sendIp(NetMessage.Ip ip, MessageAnno anno) {

    flowStore.put(ip, anno);

    // TODO
//    if (sendToMacEvent.isActive())
//      sendToMacEvent.handle(ip, anno, localAddr, qmsg.getNextHop());
    try {
      this.sendToClick(ip, null, ClickInterface.IFID_KERNELTAP,
          ClickInterface.SIMCLICK_PTYPE_IP);
    } catch (ClickException e) {
      throw new RuntimeException(e);
    }
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.net.NetInterface#receive(jist.swans.misc.Message, jist.swans.mac.MacAddress, byte, boolean, jist.swans.misc.MessageAnno)
   */
  public void receive(Message msg, MacAddress lastHop, byte macId,
      boolean promisc, MessageAnno anno)
  {
    if (Main.ASSERT) {
      Util.assertion(msg instanceof MacMessage);
      Util.assertion(null != msg);
    }
    // TODO packet loss
//    NetMessage.Ip ipmsg = (NetMessage.Ip)msg;
//    if(incomingLoss.shouldDrop(ipmsg)) {
//      if (linkDropEvent.isActive())
//        linkDropEvent.handle(msg, anno, localAddr, lastHop, true);
//      return;
//    }

    if(log.isInfoEnabled()) {
      log.info("receive t="+JistAPI.getTime()+" from="+lastHop+" on="+macId+" data="+msg);
    }
    if (recvFromMacEvent.isActive())
      recvFromMacEvent.handle(msg, anno, localAddr, lastHop);

    try {
      sendToClick(msg, anno, macId, getNicInfo(macId).encap);
    } catch (ClickException e) {
      throw new RuntimeException(e);
    }
  }

  //////////////////////////////////////////////////
  // send pump
  //

  /*
   * (non-Javadoc)
   * @see jist.swans.net.NetInterface#endSend(jist.swans.misc.Message, int, jist.swans.misc.MessageAnno)
   */
  public void endSend(Message packet, int interfaceId, MessageAnno anno)
  {
    ClickNicInfo ni = (ClickNicInfo) nics[interfaceId];
    byte[] data = null;

    // feedback packet
    if (ni.txfeedback) {
      Message macmsg = packet;

      if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA) {
        macmsg = ((MacClickMessage.ExtraHeader) ni.packet).body;
      }

      // Should be the same object here, even the references should match
      if (Main.ASSERT)
        Util.assertion(packet == macmsg);

      // reuse the exising byte array in the raw message
      if (macmsg instanceof MacClickMessage.AbstractMessageWithBody) {
        MacClickMessage.AbstractMessageWithBody mgt = (MacClickMessage.AbstractMessageWithBody) macmsg;
        if (mgt.getBody() instanceof MacClickMessage.RawMessage) {
          data = ((MacClickMessage.RawMessage)mgt.getBody()).getBytes();
        }
      }

      // generate bytes, if not a raw message
      if (null == data) {
        data = new byte[ni.packet.getSize()];
        ni.packet.getBytes(data, 0);
      }

      // if click expects extra headers, generate the appropriate one
      if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA) {
        int bitrate = ((Integer)anno.get(MessageAnno.ANNO_MAC_BITRATE)).intValue();

        int flags = MacClickMessage.ExtraHeader.WIFI_EXTRA_TX;
        if (anno.get(MessageAnno.ANNO_MAC_SUCCESS).equals(Boolean.FALSE))
          flags |= MacClickMessage.ExtraHeader.WIFI_EXTRA_TX_FAIL;
        byte rate = (byte)(bitrate * 2 / 1000000);
        byte rate1 = 0;
        byte max_tries = 0; // TODO
        byte max_tries1 = 0;
        byte retries = ((Byte)anno.get(MessageAnno.ANNO_MAC_RETRIES)).byteValue();
        byte virt_col = 0;

        MacClickMessage.ExtraHeader.updateFeedbackedPacket(data, 0, flags, rate, rate1,
            max_tries, max_tries1, retries, virt_col);
      }

    }

    ni.packet = null;
    nics_ready[interfaceId] = true;

    // feedback to click, even if not ni.txfeedback set, because we must
    // restart the tosimdevice element!
    try {
      clickInterface.send(interfaceId, ni.encap, data, true);
    } catch (ClickException e) {
      throw new RuntimeException(e);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  //////////////////////////////////////////////////
  // receive
  //

  public void recvFromClick(int ifid, byte[] data, int type) {
    // substitute with nic info encap
    if (type == ClickInterface.SIMCLICK_PTYPE_UNKNOWN)
      type = getNicInfo(ifid).encap;

    // Goes the packet up or down the stack?
    boolean directionUp = ClickInterface.IFID_KERNELTAP == ifid;
    try {
      Message msg = click.runtime.Util.fromBytes(data, type, !directionUp);
      if (directionUp)
        recvFromClickSendUp(msg, new MessageAnno(), ifid);
      else
        recvFromClickSendDown(msg, new MessageAnno(), ifid);
    } catch (ClickException e) {
      throw new RuntimeException(e);
    } catch (IllegalFormatException e) {
      HexDumpEncoder encoder = new HexDumpEncoder();
      log.warn(this+"("+JistAPI.getTime()+"): format problems for message"
          +" (msg=" + e.getMessage()+", type="+type+")\n"+encoder.encode(data));
    }
  }

  protected void recvFromClickSendUp(Message msg, MessageAnno anno,
      int ifid) throws ClickException {
    // Unmarshall packet
    if (msg instanceof MacClickMessage.ExtraHeader) {
      MacClickMessage.ExtraHeader exh = (MacClickMessage.ExtraHeader)msg;
      msg = exh.body;
      anno.put(MessageAnno.ANNO_MAC_BITRATE, new Integer(exh.rate * 1000000 / 2));
      // TODO check for errors
      anno.put(MessageAnno.ANNO_RADIO_RECV_STATUS, Constants.RADIO_RECV_STATUS_OK);

      // Conversion for Atheros
      // Unlike the other vendors described, Atheros uses a formula to derive
      // dBm.
      // RSSI_Max = 60
      // Convert % to RSSI
      // Subtract 95 from RSSI to derive dBm
      // Notice that this gives a dBm range of -35dBm at 100% and -95dBm at
      // 0%.

      anno.put(MessageAnno.ANNO_RADIO_SIGNAL_POWER, Double.valueOf(Util.fromDB(exh.rssi - 95.)));
      anno.put(MessageAnno.ANNO_RADIO_PACKET_SNR, Double.valueOf(Util.fromDB(exh.rssi)));
      anno.put(MessageAnno.ANNO_MAC_RF_CHANNEL, RFChannel.DEFAULT_RF_CHANNEL);
    }

    MacMessage macMsg = (MacMessage) msg;
    if (macMsg instanceof MacClickMessage.Management)
      return;

    Message msgBody = ((MacMessage.HasBody) macMsg).getBody();
    if (!(msgBody instanceof NetMessage.Ip))
      return;

    MacAddress lastHop = ((MacMessage.HasSrc) macMsg).getSrc();
    if (Main.ASSERT)
      Util.assertion(null != lastHop);
    // Prevent routing loops
    if (!isForMe((NetMessage.Ip) msgBody)) {
      log.error(this + "(" + JistAPI.getTime() + "): received packet not for me "
          + msgBody.toString());
      return; // kill packet
    }

    flowStore.get((NetMessage.Ip)msgBody, anno);

    super.receive(msgBody, lastHop, (byte)ifid, promisc, anno);
  }

  protected void recvFromClickSendDown(Message msg, MessageAnno anno,
      int ifid) throws ClickException {
    ClickNicInfo ni = getNicInfo(ifid);

    // storte the downcoming packet in interface struct
    if (null != ni.packet || !nics_ready[ifid]) {
      throw new ClickException("Got packet during anothers packet transmission.");
    }

    // Unmarshall packet, use raw encoding
    if (Main.ASSERT) {
      switch (ni.encap) {
      case  ClickInterface.SIMCLICK_PTYPE_WIFI:
        Util.assertion(msg instanceof MacMessage);
        break;
      case  ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA:
        Util.assertion(msg instanceof ExtraHeader);
        break;
      case  ClickInterface.SIMCLICK_PTYPE_ETHER:
      case  ClickInterface.SIMCLICK_PTYPE_IP:
        throw new ClickException("Not implemented yet");
      case  ClickInterface.SIMCLICK_PTYPE_UNKNOWN:
      default:
        throw new ClickException("Unknown packet type");
      }
    }

    ni.packet = msg;
    nics_ready[ifid] = false;

    // create send annotations
    MacMessage.HasDst dstMsg = (HasDst)
        ((MacMessage)msg).getAdapter(MacMessage.HasDst.class);
    if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA) {
      MacClickMessage.ExtraHeader exh = (MacClickMessage.ExtraHeader)msg;
      msg = exh.body;
      if (0 != exh.rate)
        anno.put(MessageAnno.ANNO_MAC_BITRATE, new Integer(exh.rate * 1000000 / 2));
      //anno.put(RadioInterface.ANNO_SIGNAL_POWER, power); TODO power
    }

    // TODO set correct queue length
//    MacMessage.HasBody msgBody = (MacMessage.HasBody)
//        macmsg.getAdapter(MacMessage.HasBody.class);
//    if (null != msgBody) {
    if (enqueueEvent.isActive())
      enqueueEvent.handle(msg, anno, localAddr, dstMsg.getDst(), null);
    if (dequeueEvent.isActive())
      dequeueEvent.handle(msg, anno, localAddr, dstMsg.getDst(), null);
    if (sendToMacEvent.isActive())
      sendToMacEvent.handle(msg, anno, localAddr, dstMsg.getDst());
//    }

    if(log.isInfoEnabled()) {
      log.info("send t="+JistAPI.getTime()+" to="+dstMsg.getDst()+" data="+ni.packet);
    }

      // schedule and send the packet down
    JistAPI.sleep(Constants.NET_DELAY);
    ni.mac.send(msg, dstMsg.getDst(), anno);
  }

  protected void sendToClick(Message msg, MessageAnno anno, int ifid,
      int encapType) throws ClickException {

    byte[] msgBytes = null;

    // Packet goes down
    if (ClickInterface.IFID_KERNELTAP == ifid
        || encapType == ClickInterface.SIMCLICK_PTYPE_ETHER)
    {
      msgBytes = new byte[msg.getSize()];
      msg.getBytes(msgBytes, 0);
    }
    else // Packet goes up
    {
      ClickNicInfo ni = (ClickNicInfo) nics[ifid];

      // if it is a message with raw body, we still have the complete byte array
      // we recycle it instead of marshaling twice.
      // Assumption: all interfaces use the same encoding
      if (msg instanceof MacClickMessage.AbstractMessageWithBody) {
        MacClickMessage.AbstractMessageWithBody mgt = (MacClickMessage.AbstractMessageWithBody) msg;
        if (mgt.getBody() instanceof MacClickMessage.RawMessage) {
          msgBytes = ((MacClickMessage.RawMessage) mgt.getBody()).getBytes();

          // Update the 802.11 header, if necessary
          int offset = 0;
          if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA)
            offset = MacClickMessage.ExtraHeader.HEADER_SIZE;
          mgt.updateRecvPacket(msgBytes, offset);
        }
      }

      // if we were not able to recycle, marshal again...
      if (null == msgBytes) {
        // add extra header, if necessary
        if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA) {
          msg = msgFactory.createExraHeader(0, (byte) 0, (byte) 0,
              (MacClickMessage) msg);
        }

        msgBytes = new byte[msg.getSize()];
        msg.getBytes(msgBytes, 0);
      }

      // Update the extra header, if any
      if (ni.encap == ClickInterface.SIMCLICK_PTYPE_WIFI_EXTRA) {
        double signalPower = ((Double) anno
            .get(MessageAnno.ANNO_RADIO_SIGNAL_POWER)).doubleValue();
        // double noiseLevel =
        // ((Double)anno.get(RadioInterface.ANNO_NOISE_LEVEL)).doubleValue();
        int bitrate = ((Integer) anno.get(MessageAnno.ANNO_MAC_BITRATE))
            .intValue();

        int flags = 0; // all went well ... so far

        // Conversion for Atheros
        // Unlike the other vendors described, Atheros uses a formula to derive
        // dBm.
        // RSSI_Max = 60
        // Convert % to RSSI
        // Subtract 95 from RSSI to derive dBm
        // Notice that this gives a dBm range of -35dBm at 100% and -95dBm at
        // 0%.

        byte rssi = (byte) Math.min(10 * Math.log(signalPower)
            / Constants.log10 + 95, 60.);

        // convert bitrate to click representation
        byte rate = (byte) (bitrate * 2 / 1000000);

        BitErrorData errorBytes = BitErrorMask.apply(msgBytes, anno);
        msgBytes = errorBytes.data;
        if (errorBytes.plcpErrors || msgBytes.length < 24) // 24 is the length of the WIFI header
          return;


        // WIFI_EXTRA_RX_ERR is set if errors occur
        // Actually WIFI_EXTRA_RX_ERR is 1 << 3, but as we shuffle the bytes
        // around in
        // updateRecvPacket in a (probably non-portable) attempt to mitigate
        // endianness
        // problems I have to set it the other way round here.
        // TODO: how can I demystify this?
        if (errorBytes.dataErrors)
          flags |= 1 << 27;

        MacClickMessage.ExtraHeader.updateRecvPacket(msgBytes, 0, flags, rssi,
            rate);
      }
    }

    try {
      clickInterface.send(ifid, encapType, msgBytes, false);
    } catch (IOException e) {
      // TODO Auto-generated catch block
      throw new ClickException(e);
    }
  }

  /**
   * Access a click read handler from Java.
   *
   * @param elemName
   * @param handlerName
   * @throws ClickException
   */
  public String readHandler(
      String elemName,
      String handlerName) throws ClickException {
    try {
      return clickInterface.readString(elemName, handlerName);
    } catch (IOException e) {
      // TODO Auto-generated catch block
      throw new ClickException(e);
    }
  }

  /**
   * Access a click write handler.
   *
   * @param elemName
   * @param handlerName
   * @param value
   * @throws ClickException
   */
  public int writeHandler(
      String elemName,
      String handlerName,
      String value) throws ClickException {
    try {
      return clickInterface.write(elemName, handlerName, value);
    } catch (IOException e) {
      // TODO Auto-generated catch block
      throw new ClickException(e);
    }
  }

  //////////////////////////////////////////////////
  // Overwrites
  //

  public void setRouting(RouteInterface routeProxy) {
    throw new RuntimeException("invalid method in control path");
  }


}
