package brn.sim.handler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jist.swans.Constants;
import jist.swans.field.AbstractField;
import jist.swans.field.AbstractField.NodeMoveEvent;
import jist.swans.mac.MacAddress;
import jist.swans.misc.Event;
import jist.swans.misc.Location;
import jist.swans.misc.MessageAnno;
import jist.swans.misc.Util;
import jist.swans.net.NetAddress;

import org.apache.log4j.Logger;

import brn.sim.DataManager;
import brn.sim.DriverRemote;
import brn.sim.DataManager.DataContribution;
import brn.sim.DataManager.DataContributor;
import brn.sim.data.AbstractDiagramData;
import brn.sim.data.AveragedTimeLine;
import brn.sim.data.DiagramData;
import brn.sim.data.Line;
import brn.sim.data.LinkTableData;
import brn.sim.data.TimeLine;
import brn.sim.data.XplotSerializer;
import brn.swans.route.metric.AbstractRouteMetric;
import brn.swans.route.metric.LinkProbe;
import brn.swans.route.metric.LinkTable;
import brn.util.Graphviz;

public class LinkQualityHandler extends DataContributor {

  public static final Logger log = Logger.getLogger(LinkQualityHandler.class.getName());

  public static final String ID = "LinkQualityHandler";

  /**
   * Capsulates link quality related information for the specified link on
   * the specified bit-rate.
   *
   * @author kurth
   */
  public class LinkQualityData {

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

    /** id of source node */
    public int srcId;
    /** mac address of neighbour. */
    public int dstId;
    public int rate;

    private DiagramData arrivalOk;
    private DiagramData arrivalCrc;
    private DiagramData arrivalPhy;
    private DiagramData arrivalAll;

    private DiagramData rssiOk;
    private DiagramData rssiCrc;
    private DiagramData rssiPhy;

    private DiagramData rssiOkAvg;

    double sampleLen = 1. /* sec */;
    double scale = 1.0;

    //////////////////////////////////////////////////
    // initialize
    //

    public LinkQualityData(int srcId, int dstId, int rate, double sampleLen, double scale) {
      super();
      this.srcId = srcId;
      this.dstId = dstId;
      this.rate = rate;
      this.sampleLen = sampleLen;
      this.scale = scale;
    }


    //////////////////////////////////////////////////
    // accessors
    //

    /**
     * @return the arrivalAll
     */
    public DiagramData getArrivalAll() {
      if (null == arrivalAll) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "Link Quality (all packets)" };
        arrivalAll = new DiagramData(
            new AveragedTimeLine(path[path.length-1], sampleLen, AveragedTimeLine.MODE_WA2, scale),
            path, "time", "sec", "pdr", "packets/sec");
        addDiagramData(arrivalAll, DataManager.LEVEL_ADDITIONAL);
      }

      return arrivalAll;
    }

    /**
     * @return the arrivalCrc
     */
    public DiagramData getArrivalCrc() {
      if (null == arrivalCrc) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "Link Quality (crc packets)" };
        arrivalCrc = new DiagramData(
            new AveragedTimeLine(path[path.length-1], sampleLen, AveragedTimeLine.MODE_WA2, 1.),
            path, "time", "sec", "pdr", "packets/sec");
        addDiagramData(arrivalCrc, DataManager.LEVEL_ADDITIONAL);
      }

      return arrivalCrc;
    }

    /**
     * @return the arrivalOk
     */
    public DiagramData getArrivalOk() {
      if (null == arrivalOk) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "Link Quality (correct packets)" };
        arrivalOk = new DiagramData(
            new AveragedTimeLine(path[path.length-1], sampleLen, AveragedTimeLine.MODE_WA2, scale),
            path, "time", "sec", "pdr", "packets/sec");
        addDiagramData(arrivalOk, DataManager.LEVEL_BASIC);
      }

      return arrivalOk;
    }

    /**
     * @return the arrivalPhy
     */
    public DiagramData getArrivalPhy() {
      if (null == arrivalPhy) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "Link Quality (phy packets)" };
        arrivalPhy = new DiagramData(
            new AveragedTimeLine(path[path.length-1], sampleLen, AveragedTimeLine.MODE_WA2, scale),
            path, "time", "sec", "pdr", "packets/sec");
        addDiagramData(arrivalPhy, DataManager.LEVEL_ADDITIONAL);
      }

      return arrivalPhy;
    }

    /**
     * @return the id
     */
    public int getId() {
      return srcId;
    }

    /**
     * @return the rate
     */
    public int getRate() {
      return rate;
    }

    /**
     * @return the rssiCrc
     */
    public DiagramData getRssiCrc() {
      if (null == rssiCrc) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "SNR (crc packets)"};
        rssiCrc = new DiagramData(new TimeLine(path[path.length-1]),
            path, "time", "sec", "SNR", "dBm", AbstractDiagramData.MARK);
        addDiagramData(rssiCrc, DataManager.LEVEL_ALL);
      }

      return rssiCrc;
    }

    /**
     * @return the rssiOk
     */
    public DiagramData getRssiOk() {
      if (null == rssiOk) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "SNR (ok packets)"};
        rssiOk = new DiagramData(new TimeLine(path[path.length-1]),
            path, "time", "sec", "SNR", "dBm", AbstractDiagramData.MARK);
        addDiagramData(rssiOk, DataManager.LEVEL_ALL);
        
        path[path.length-1] = "SNR (ok packets) avg'd";
        rssiOkAvg = new DiagramData(
            new AveragedTimeLine(path[path.length-1], sampleLen, AveragedTimeLine.MODE_A, scale),
            path, "time", "sec", "SNR", "dBm");
        addDiagramData(rssiOkAvg, DataManager.LEVEL_BASIC);
        rssiOk.addChain(rssiOkAvg);
      }
      return rssiOk;
    }

    /**
     * @return the rssiPhy
     */
    public DiagramData getRssiPhy() {
      if (null == rssiPhy) {
        String[] path = new String[] {
            "Node " + Integer.valueOf(srcId), "Links",
            "Link " + dstId + " " + srcId,
            String.valueOf(rate/Constants.BANDWIDTH_1Mbps) + " Mbps",
            "SNR (phy packets)"};
        rssiPhy = new DiagramData(new TimeLine(path[path.length-1]),
            path, "time", "sec", "SNR", "dBm", AbstractDiagramData.MARK);
        addDiagramData(rssiPhy, DataManager.LEVEL_ALL);
      }

      return rssiPhy;
    }

    /**
     * @return the scale
     */
    public double getScale() {
      return scale;
    }

    public void addDiagramData(AbstractDiagramData data, int level) {
      addData(data, level);
    }

  }

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

  private Map mapNodes;

  /** list of neighbours. */
  private Map[] /*nodeId -> Map<MacAddress,LinkQualityData>*/ mapLinks;

  private double sampleLen = 1. /* sec */;

  private Map mapIdToBitrate;

  private Map mapIdToSignalStrength;

  private double scale = 1.;

  //////////////////////////////////////////////////
  // initialize
  //


  /**
   * Constructs a new test object.
   */
  public LinkQualityHandler() {
    this.mapLinks = new HashMap[20];
    this.mapNodes = new HashMap();

    this.mapIdToBitrate = new HashMap();
    this.mapIdToSignalStrength = new HashMap();
  }

  public double getSampleLen() {
    return sampleLen;
  }

  public void setSampleLen(double sampleLen) {
    this.sampleLen = sampleLen;
  }


  public void registerHandlers() {
    super.registerHandlers();

    Event.addHandler(AbstractRouteMetric.LinkProbeArrivedEvent.class, new Event.Handler() {
      public void handle(Event event) {
        AbstractRouteMetric.LinkProbeArrivedEvent ev = 
          (AbstractRouteMetric.LinkProbeArrivedEvent)event;
        
        LinkProbe lp  = (LinkProbe) ev.data;
        sampleLen = /*to sec*/ lp.getTau() / 1000.;
        scale = (double)lp.getPeriod() / (double)lp.getTau();
        handleReceive(ev.anno, ev.nodeId, lp.getIp().getId(), ev.time);
      }
    });

    Event.addHandler(AbstractField.NodeMoveEvent.class, new Event.Handler() {
      public void handle(Event event) {
        handleNodeMove((AbstractField.NodeMoveEvent) event);
      }
    });

//    Event.addHandler(AbstractMac.ReceiveEvent.class, new Event.Handler() {
//      public void handle(Event event) {
//        AbstractMac.ReceiveEvent ev = (AbstractMac.ReceiveEvent) event;
//        
//        if (ev.data instanceof MacMessage.HasSrc)
//          handleReceive(ev, ((MacMessage.HasSrc)ev.data).getSrc(), ev.time);
//      }
//    });
  }


  //////////////////////////////////////////////////
  // measurements
  //

  protected void handleNodeMove(NodeMoveEvent event) {
    LinkTableData.Node node = (LinkTableData.Node) mapNodes.get(new Integer(event.nodeId));
    if (null == node) {
      node = new LinkTableData.Node(event.nodeId, event.loc.getX(), event.loc.getY());
      mapNodes.put(new Integer(event.nodeId), node);
    }

    node.locX = event.loc.getX();
    node.locY = event.loc.getY();
  }

  protected void handleReceive(MessageAnno anno, int srcId, int dstId, long time) {
    Integer bitRate = (Integer)anno.get(MessageAnno.ANNO_MAC_BITRATE);
    if (null == bitRate) {
      throw new RuntimeException("Bitrate not found");
    }
      
    LinkQualityData n = getLinkQualityData(srcId, dstId, bitRate);

    //n.probeArrival.addNextTimePoint(new Long(JistAPI.getTime()));
    Byte status = (Byte) anno.get(MessageAnno.ANNO_RADIO_RECV_STATUS);
//    if (null == status)
//      status = Constants.RADIO_RECV_STATUS_OK;

    if (status.equals(Constants.RADIO_RECV_STATUS_LOST)) {
      n.getArrivalOk().addNextTimePoint(time, .0);
      n.getArrivalCrc().addNextTimePoint(time, .0);
      n.getArrivalPhy().addNextTimePoint(time, .0);
      n.getArrivalAll().addNextTimePoint(time, .0);
    }
    else {
      n.getArrivalAll().addNextTimePoint(time, 1.);
      double signalPower = Util.toDB(((Double)anno.
          get(MessageAnno.ANNO_RADIO_SIGNAL_POWER)).doubleValue());

      if (status.equals(Constants.RADIO_RECV_STATUS_OK)) {
        n.getRssiOk().addNextTimePoint(time, signalPower);
        n.getArrivalOk().addNextTimePoint(time, 1.);
        n.getArrivalCrc().addNextTimePoint(time, .0);
        n.getArrivalPhy().addNextTimePoint(time, .0);
      }
      else if (status.equals(Constants.RADIO_RECV_STATUS_CRC)) {
        n.getRssiCrc().addNextTimePoint(time, signalPower);
        n.getArrivalOk().addNextTimePoint(time, .0);
        n.getArrivalCrc().addNextTimePoint(time, 1.);
        n.getArrivalPhy().addNextTimePoint(time, .0);
      }
      else if (status.equals(Constants.RADIO_RECV_STATUS_PHY)) {
        n.getRssiPhy().addNextTimePoint(time, signalPower);
        n.getArrivalOk().addNextTimePoint(time, .0);
        n.getArrivalCrc().addNextTimePoint(time, .0);
        n.getArrivalPhy().addNextTimePoint(time, 1.);
      }
    }
  }

  /**
   * Find the link quality data to the given keys.
   *
   * @param srcId node id.
   * @param lastHop last hop from which the packet came.
   * @param bitRate the used bit rate.
   * @return the link quality data to the given keys.
   */
  private LinkQualityData getLinkQualityData(int srcId, int dstId, Integer bitRate) {
    Map neighbours = null;
    try {
      neighbours = mapLinks[srcId];
    } catch (ArrayIndexOutOfBoundsException e) {
      Map[] newMap = new HashMap[srcId + mapLinks.length];
      System.arraycopy(mapLinks, 0, newMap, 0, mapLinks.length);
      mapLinks = newMap;
    }
    if (null == neighbours) {
      neighbours = new HashMap();
      mapLinks[srcId] = neighbours;
    }

    Map mapRates = (Map)neighbours.get(dstId);
    if(mapRates==null) {
      mapRates = new HashMap(13);
      neighbours.put(dstId, mapRates);
    }

    LinkQualityData n = (LinkQualityData)mapRates.get(bitRate);
    if (n==null) {
      n = new LinkQualityData(srcId, dstId, bitRate.intValue(), sampleLen, scale );
      mapRates.put(bitRate, n);
      int bandWidthMbps = bitRate.intValue()/Constants.BANDWIDTH_1Mbps;

      // Create an gui item for each new bit rate!
      if (!mapIdToBitrate.containsValue(bitRate)) {
        int id = addData(new String[] { "Global", "Links",
            "Link Quality " + bandWidthMbps + " Mbps"},
            DriverRemote.ITEM_TYPE_LINKTABLE, DataManager.LEVEL_BASIC);
        if (0 <= id)
          mapIdToBitrate.put(new Integer(id), bitRate);
      }

      if (!mapIdToSignalStrength.containsValue(bitRate)) {
        int id = addData(new String[] { "Global", "Links",
            "Signal Strength " + bandWidthMbps + " Mbps"},
            DriverRemote.ITEM_TYPE_LINKTABLE, DataManager.LEVEL_ADDITIONAL);
        if (0 <= id)
          mapIdToSignalStrength.put(new Integer(id), bitRate);
      }
    }
    return n;
  }


  //////////////////////////////////////////////////
  // measurements
  //

  /**
   * Generate a LinkTable Graph vor viz purposes.
   * @param id node id
   * @return the graph structure
   */
  private LinkTableData generateLinkTable(int id) {
    Integer bitRate = (Integer) mapIdToBitrate.get(new Integer(id));
    boolean signalStrength = false;
    if (null == bitRate) {
      bitRate = (Integer) mapIdToSignalStrength.get(new Integer(id));
      signalStrength = true;
    }

    List lstNodes = new ArrayList(mapNodes.values());
    List lstLinks = new ArrayList();

    for (int i = 0; i < mapLinks.length; i++) {
      Map neighbours = mapLinks[i];
      if (null == neighbours)
        continue;

      Iterator iter = neighbours.values().iterator();
      while (null != iter && iter.hasNext()) {
        Map mapRates = (Map) iter.next();

        LinkQualityData n = (LinkQualityData)mapRates.get(bitRate);
        if (null == n || null == n.getArrivalOk())
          continue;

//        Line line = null;
//        if (signalStrength)
//          line = n.rssiOkAvg.getLine();
//        else
//          line = n.getArrivalOk().getLine();
//
//        // at least 5 points !!
//        if (5 >= line.getLengthInternal())
//          continue;
//
//        // TODO get real net addresses
//        int metric = 0;
//        if (signalStrength)
//          metric = (int)line.getYInternal()[line.getLengthInternal()-1];
//        else
//          metric = (int)(100. / line.getYInternal()[line.getLengthInternal()-1]);
//        lstLinks.add(new LinkTableData.Link(new NetAddress(n.srcId),
//            new NetAddress(n.dstId), metric, 0));
        Line line = n.rssiOk.getLine();

        // at least 5 points !!
        if (5 >= line.getLengthInternal())
          continue;

        
        // TODO get real net addresses
        int metric = 0;
        if (signalStrength)
          metric = (int)AveragedTimeLine.aggregate(line, 
              AveragedTimeLine.MODE_A, 1.); 
        else
          metric = (int)(100. / AveragedTimeLine.aggregate(line, 
              AveragedTimeLine.MODE_R, n.scale));
        lstLinks.add(new LinkTableData.Link(new NetAddress(n.srcId),
            new NetAddress(n.dstId), metric, 0));
      }
    }

    LinkTableData ret = new LinkTableData(0, lstNodes, lstLinks);
    return ret;
  }

  private Graphviz toGraphViz(LinkTableData linkTable) {
    Graphviz.DiGraph linkTableGraph = new Graphviz.DiGraph(50, 100, false);

    final double scaleX = 10;
    final double scaleY = 2;
    final double f_y = 0;//2;

    // build hash map of nodes
    Map mapNodes = new HashMap();
    List nodes = linkTable.getNodes();
    for (int i = 0; i < nodes.size(); i++) {
      LinkTableData.Node node = (LinkTableData.Node) nodes.get(i);
      mapNodes.put(new Integer(node.nodeId), node);
    }

    // iterate through all links
    Collection coll = linkTable.getLinks();
    Iterator iter = coll.iterator();
    while (iter.hasNext()) {
      double offset = f_y;
      LinkTable.LinkData link = (LinkTable.LinkData) iter.next();

      NetAddress fromAddr = (NetAddress) link.from();
      NetAddress toAddr = (NetAddress) link.to();
      int metric = link.metric();

      LinkTableData.Node nodeFrom = (LinkTableData.Node) mapNodes.get(
          new Integer(fromAddr.getId()));
      Location locFrom = (null == nodeFrom ? null :
        new Location.Location2D(nodeFrom.locX, nodeFrom.locY));

      LinkTableData.Node nodeTo = (LinkTableData.Node) mapNodes.get(
          new Integer(toAddr.getId()));
      Location locTo = (null == nodeTo ? null :
        new Location.Location2D(nodeTo.locX, nodeTo.locY));

      if (locFrom != null && locTo != null) {
        String from = fromAddr.toString();
        String to = toAddr.toString();

        linkTableGraph.addNode(from, scaleX * locFrom.getX(),
            scaleY * locFrom.getY() + offset, 0);
        linkTableGraph.addNode(to, scaleX * locTo.getX(),
            scaleY * locTo.getY() + offset, 0);
        linkTableGraph.addLink(from, to, metric);
      }
    }

    return linkTableGraph;
  }

  private void saveAll() throws IOException {
    long avgIntv = 10 * Constants.SECOND;
    int mode = AveragedTimeLine.MODE_C;
    double scale = 1.0;
    boolean found = false;

    for (int i = 0; i < mapLinks.length; i++) {
      Map neighbours = mapLinks[i];
      if (neighbours == null)
        continue;

      // process neighbour set
      Iterator it = neighbours.values().iterator();
      while(it.hasNext()) {
        found = true;
        Map mapRates = (Map) it.next();
        Iterator iter = mapRates.values().iterator();
        while (iter.hasNext()) {
          LinkQualityData n = (LinkQualityData)iter.next();
          int rate = n.rate / Constants.BANDWIDTH_1Mbps;

          // save averaged time lines
          XplotSerializer seri = new XplotSerializer("packet arrival", "time", "packets");
          seri.addLine(new AveragedTimeLine((TimeLine)n.getArrivalOk().getLine(), avgIntv, mode, scale), "green");
          seri.addLine(new AveragedTimeLine((TimeLine)n.getArrivalCrc().getLine(), avgIntv, mode, scale), "yellow");
          seri.addLine(new AveragedTimeLine((TimeLine)n.getArrivalPhy().getLine(), avgIntv, mode, scale), "red");
          seri.addLine(new AveragedTimeLine((TimeLine)n.getArrivalAll().getLine(), avgIntv, mode, scale), "blue");
          seri.addPoints((TimeLine)n.getRssiOk().getLine(), "green");
          seri.addPoints((TimeLine)n.getRssiCrc().getLine(), "yellow");
          seri.addPoints((TimeLine)n.getRssiPhy().getLine(), "red");
          seri.saveToFile(n.srcId + "-" + n.dstId + "-" + rate + "Mbps.xpl");
        }
      }
    }

    if (false == found) {
      log.error("No recorded link quality found, forgotten to create link table?");
    }
  }

  public AbstractDiagramData getTimeline(int src, MacAddress dst, int rate, int status) {
    Map neighbours = mapLinks[src];
    Map mapRates = (Map) neighbours.get(dst);
    if (null == mapRates)
      return null;

    LinkQualityData n = (LinkQualityData)mapRates.get(new Integer(rate));
    if (null == n)
      return null;

    switch (status) {
    case 0: return n.getArrivalOk();
    case 1: return n.getArrivalCrc();
    case 2: return n.getArrivalPhy();
    case 3: return n.getArrivalAll();
    }
    return null;
  }

  /*
   * (non-Javadoc)
   * @see brn.sim.DataManager.DataContributor#getContribution(int)
   */
  public DataContribution getContribution(int id, String[] path) {
    return generateLinkTable(id);
  }

  /*
   * (non-Javadoc)
   * @see brn.sim.DataManager.DataContributor#getId()
   */
  public String getId() {
    return ID;
  }

  /*
   * (non-Javadoc)
   * @see brn.sim.DataManager.DataContributor#getContent(int, brn.sim.DataManager.DataContribution)
   */
  public String getContent(int id, DataContribution contribution) throws Exception {
    if (mapIdToBitrate.containsKey(new Integer(id))
        || mapIdToSignalStrength.containsKey(new Integer(id)))
      return toGraphViz(generateLinkTable(id)).getText();

    return super.getContent(id, contribution);
  }

  /*
   * (non-Javadoc)
   * @see brn.sim.DataManager.DataContributor#getFileName(int, brn.sim.DataManager.DataContribution)
   */
  public String getFileName(int id, String[] path, DataContribution contribution) {
    if (mapIdToBitrate.containsKey(new Integer(id))
        || mapIdToSignalStrength.containsKey(new Integer(id)))
      return DataManager.getPath(path) + ".dot";

    return super.getFileName(id, path, contribution);
  }
}
