package brn.swans.route;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.runtime.Util;
import jist.swans.Constants;
import jist.swans.mac.MacAddress;
import jist.swans.misc.Message;
import jist.swans.misc.MessageAnno;
import jist.swans.net.NetAddress;
import jist.swans.net.NetInterface;
import jist.swans.net.NetMessage;
import jist.swans.net.NetMessage.Ip;
import jist.swans.route.AbstractRoute;
import jist.swans.route.RouteInterface;

import org.apache.log4j.Logger;

import brn.swans.route.metric.ArpTableInterface;

/**
 * Table-based routing agent. Performs ARP for MAC address lookup.
 *
 * @author kurth
 */
public class RouteTable extends AbstractRoute implements RouteTableInterface {

  private static final int ARP_MAX_RETRY = 4;

  private static final long ARP_RETRY_TIMEOUT = 100 * Constants.MILLI_SECOND;

  private static Logger log = Logger.getLogger(RouteTable.class.getName());

  protected static class TableEntry {
    public byte[] netDest;
    public byte[] netMask;
    public NetAddress gateway;
    public int device;

    public boolean matchesNet(NetAddress dst) {
      byte[] dstBytes = dst.getBytes();
      for (int i = 0; i < dstBytes.length; i++)
        if ((dstBytes[i] & netMask[i]) != netDest[i])
          return false;
      return true;
    }
  }

  public static class ArpMessage implements Message {

    /**      ARP request        */
    public static final int ARPOP_REQUEST  = 1;
    /**      ARP reply        */
    public static final int ARPOP_REPLY    = 2;

    public int op;
    /** 8-13  (22-27)  sender hardware address    */
    public MacAddress sha;
    /** 14-17 (28-31)  sender protocol address    */
    public NetAddress spa;
    /** 18-23 (32-37)  target hardware address    */
    public MacAddress tha;
    /** 24-27 (38-41)  target protocol address    */
    public NetAddress tpa;

    public ArpMessage(int op, MacAddress sha, NetAddress spa, MacAddress tha,
        NetAddress tpa) {
      super();
      this.op = op;
      this.sha = sha;
      this.spa = spa;
      this.tha = tha;
      this.tpa = tpa;
    }

    public void getBytes(byte[] msg, int offset) {
      throw new RuntimeException();
    }

    public int getSize() {
      return 42;
    }

  }

  public class BufferEntry {

    private NetAddress gateway;
    public Ip ipMsg;
    public int device;
    public MessageAnno anno;

    public BufferEntry(Ip ipMsg, MessageAnno anno, int device,
        NetAddress gateway) {
      this.ipMsg = ipMsg;
      this.anno = anno;
      this.device = device;
      this.gateway = gateway;
    }

  }

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

  /**
   * The IP address of this node.
   */
  protected NetAddress localAddr;

  /**
   * The MAC address of this node.
   */
  protected MacAddress localMac;

  /**
   * The interface to the network layer.
   */
  protected NetInterface netEntity;

  /**
   * The proxy interface for this object.
   */
  protected RouteTableInterface self;

  /**
   * Interface to the arp table.
   */
  protected ArpTableInterface arp;

  /**
   * The routing table
   */
  protected TableEntry[] routingTable;

  /**
   * maps requested ip -> packet list
   */
  protected Map activeArpRequests;

  /**
   * Whether to forward messages
   */
  protected boolean forward;


  // ////////////////////////////////////////////////
  // initialization
  //

  public RouteTable(NetAddress localAddr, ArpTableInterface arp, boolean forward) {
    this.localAddr = localAddr;
    this.localMac = arp.getArpEntry(this.localAddr);
    this.arp = arp;
    this.forward = forward;
    this.activeArpRequests = new HashMap();

    if (Main.ASSERT)
      Util.assertion(null != this.localMac);

    /*
     * NOTE: we use the artificial dlg class for proxy creation, so we can
     * support implementation inheritance.
     */
    self = (RouteTableInterface) JistAPI.proxy(
        new RouteTableInterface.Dlg(this), RouteTableInterface.class);
  }

  /*
   * (non-Javadoc)
   * @see java.lang.Object#toString()
   */
  public String toString() {
    return localAddr.toString();
  }

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

  /**
   * Sets the interface to the network layer.
   *
   * @param netEntity the interface to the network layer
   */
  public void setNetEntity(NetInterface netEntity) {
    this.netEntity = netEntity;
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.route.AbstractRoute#getRouteProxy()
   */
  public RouteInterface getRouteProxy() {
    return this.self;
  }

  public void initTable(String table) {
    String[] entries = table.split("[ \\r\\n\\t/,;]+");
    if (entries.length % 4 != 0)
      throw new RuntimeException("invalid routing table");

    routingTable = new TableEntry[entries.length/4];
    for (int i = 0; i < routingTable.length; i++) {
      routingTable[i] = new TableEntry();
      routingTable[i].netDest = new NetAddress(entries[4*i]).getBytes();
      routingTable[i].netMask = new NetAddress(entries[4*i+1]).getBytes();
      routingTable[i].gateway = new NetAddress(entries[4*i+2]);
      routingTable[i].device = Integer.parseInt(entries[4*i+3]);
    }
  }

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

  /*
   * (non-Javadoc)
   * @see jist.swans.route.RouteInterface#peek(jist.swans.net.NetMessage, jist.swans.mac.MacAddress, jist.swans.misc.MessageAnno)
   */
  public void peek(NetMessage msg, MacAddress lastHop, MessageAnno anno) {
    if (!(msg instanceof NetMessage.Ip))
      return;

    NetMessage.Ip ipMsg = (NetMessage.Ip) msg;
//    arp.addArpEntry(ipMsg.getSrc(), lastHop);
    anno.put(MessageAnno.ANNO_RTG_LASTHOP, arp.getArpEntry(lastHop));

    // TODO how to distinct b/w broadcast and intended unicast
    if (localAddr.equals(ipMsg.getDst())) {
      if (packetForwardedEvent.isActive())
        packetForwardedEvent.handle(msg, anno, arp.getArpEntry(lastHop), localAddr);
    }
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.route.RouteInterface#send(jist.swans.net.NetMessage, jist.swans.misc.MessageAnno)
   */
  public void send(NetMessage msg, MessageAnno anno) {

    NetMessage.Ip ipMsg = (NetMessage.Ip) msg;

    // TODO how to distinct b/w broadcast and intended unicast
    boolean isForwarding = !localAddr.equals(ipMsg.getSrc());
    if (isForwarding) {
      if (packetForwardedEvent.isActive())
        packetForwardedEvent.handle(msg, anno,
            (NetAddress) anno.get(MessageAnno.ANNO_RTG_LASTHOP), localAddr);
    }

    if (!forward && isForwarding)
      return;

    NetAddress dst = ipMsg.getDst();

    for (int i = 0; i < routingTable.length; i++) {
      TableEntry entry = routingTable[i];
      if (entry.matchesNet(dst)) {
        MacAddress nextHop = arp.getArpEntry(entry.gateway);

        if (null != nextHop) {
          sendToNet(ipMsg, anno, nextHop, entry.device);
        } else {
          self.sendArpRequest(ipMsg, anno, entry.device, entry.gateway, 0);
        }
        return;
      }
    }

    log.warn(this + "(" + JistAPI.getTime() + "): no route found for dst " + dst);
  }

  public void sendArpRequest(NetMessage.Ip ipMsg, MessageAnno anno,
      int device, NetAddress gateway, int retry) {
    MacAddress nextHop = arp.getArpEntry(gateway);
    if (null != nextHop) {
      sendToNet(ipMsg, anno, nextHop, device);
      List bufferedMsgs = (List) activeArpRequests.get(gateway);
      for (int i = 0; i < bufferedMsgs.size(); i++) {
        BufferEntry bufferedMsg = (BufferEntry) bufferedMsgs.get(i);
        sendToNet(bufferedMsg.ipMsg, bufferedMsg.anno, nextHop, bufferedMsg.device);
      }
      activeArpRequests.remove(gateway);
      return;
    }

    // Check if a request is currently active
    List bufferedMsg = (List) activeArpRequests.get(gateway);
    if (null != bufferedMsg && 0 == retry) {
      // if there is an active request, simply buffer and return
      bufferedMsg.add(new BufferEntry(ipMsg, anno, device, gateway));
      return;
    }
    if (null == bufferedMsg) {
//    bufferedMsg.add(new Entry(ipMsg, anno, device, gateway));
      bufferedMsg = new LinkedList();
      activeArpRequests.put(gateway, bufferedMsg);
    }

    JistAPI.sleep(Constants.random.nextInt((int)ARP_RETRY_TIMEOUT));

    ArpMessage arpRequest = new ArpMessage(ArpMessage.ARPOP_REQUEST,
        localMac, localAddr, MacAddress.NULL, gateway);
    NetMessage.Ip req = new NetMessage.Ip(arpRequest, localAddr, NetAddress.ANY,
        Constants.NET_PROTOCOL_ARP, Constants.NET_PRIORITY_D_BESTEFFORT, (byte) 1);
    netEntity.send(req, device, MacAddress.ANY, anno);

    // cancel after last arp is sent out, prevents problems with the netnotify and udp
    if (ARP_MAX_RETRY < retry) {
      log.warn(this + "(" + JistAPI.getTime() + "): ARP timeout for " + gateway);
      activeArpRequests.remove(gateway);
      return;
    }

    JistAPI.sleep(ARP_RETRY_TIMEOUT);
    self.sendArpRequest(ipMsg, anno, device, gateway, retry+1);
  }

  private void sendToNet(NetMessage.Ip ipMsg, MessageAnno anno,
      MacAddress nextHop, int device) {
    // delegate to network layer
    netEntity.send(ipMsg, device, nextHop, anno);
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.net.NetInterface.NetHandler#receive(jist.swans.misc.Message, jist.swans.net.NetAddress, jist.swans.mac.MacAddress, byte, jist.swans.net.NetAddress, byte, byte, jist.swans.misc.MessageAnno)
   */
  public void receive(Message msg, NetAddress src, MacAddress lastHop,
      byte macId, NetAddress dst, byte priority, byte ttl, MessageAnno anno) {
    ArpMessage arpMsg = (ArpMessage) msg;
    arp.addArpEntry(src, lastHop);

    if (arpMsg.op == ArpMessage.ARPOP_REQUEST && localAddr.equals(arpMsg.tpa)) {
      ArpMessage arpReply = new ArpMessage(ArpMessage.ARPOP_REPLY,
          arpMsg.sha, arpMsg.spa, localMac, localAddr);
      NetMessage.Ip rep = new NetMessage.Ip(arpReply, localAddr, NetAddress.ANY,
          Constants.NET_PROTOCOL_ARP, Constants.NET_PRIORITY_D_BESTEFFORT, (byte) 1);

      if (null != anno)
        anno = (MessageAnno) anno.clone();

      netEntity.send(rep, macId, MacAddress.ANY, anno);
    } else if (arpMsg.op == ArpMessage.ARPOP_REPLY) {
      arp.addArpEntry(arpMsg.tpa, arpMsg.tha);
    }
  }

}
