/*
 * Copyright (c) 2010, Swedish Institute of Computer Science.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $Id: DirectedGraphMedium.java,v 1.8 2010/12/02 15:25:50 fros4943 Exp $
 */

package se.sics.cooja.radiomediums;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Random;

import org.apache.log4j.Logger;
import org.jdom.Element;

import se.sics.cooja.ClassDescription;
import se.sics.cooja.Mote;
import se.sics.cooja.RadioConnection;
import se.sics.cooja.Simulation;
import se.sics.cooja.interfaces.Radio;
import se.sics.cooja.plugins.DGRMConfigurator;
import se.sics.cooja.plugins.Visualizer;
import se.sics.cooja.plugins.skins.DGRMVisualizerSkin;

/**
 * Directed Graph Radio Medium.
 * 
 * Can be used both stand-alone as a radio medium, and 
 * as a basis for other radio medium implementations.
 * 
 * The stand-alone radio medium supports propagation delays and
 * and per-link transmission success ratio/RSSI.
 * 
 * @see AbstractRadioMedium
 * @author Fredrik Osterlind
 */
@ClassDescription("Directed Graph Radio Medium (DGRM)")
public class DirectedGraphMedium extends AbstractRadioMedium {
  private static Logger logger = Logger.getLogger(DirectedGraphMedium.class);

  private Simulation simulation;
  private Random random;

  private ArrayList<Edge> edges = new ArrayList<Edge>();
  private boolean edgesDirty = true;

  /* Used for optimizing lookup time */
  private Hashtable<Radio,DestinationRadio[]> edgesTable = new Hashtable<Radio,DestinationRadio[]>();

  public DirectedGraphMedium() {
    /* Do not initialize radio medium: use only for hash table */
    super(null);
    Visualizer.registerVisualizerSkin(DGRMVisualizerSkin.class);
  }

  public DirectedGraphMedium(Simulation simulation) {
    super(simulation);
    this.simulation = simulation;
    random = simulation.getRandomGenerator();

    requestEdgeAnalysis();

    /* Register plugin and visualizer skin */
    simulation.getGUI().registerPlugin(DGRMConfigurator.class);
    Visualizer.registerVisualizerSkin(DGRMVisualizerSkin.class);
  }

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

    /* Unregister plugin and visualizer skin */
    simulation.getGUI().unregisterPlugin(DGRMConfigurator.class);
    Visualizer.unregisterVisualizerSkin(DGRMVisualizerSkin.class);
  }

  public void addEdge(Edge e) {
    edges.add(e);
    requestEdgeAnalysis();

    ((AbstractRadioMedium.RadioMediumObservable)
        this.getRadioMediumObservable()).setRadioMediumChangedAndNotify();
  }

  public void removeEdge(Edge edge) {
    if (!edges.contains(edge)) {
      logger.fatal("Cannot remove edge: " + edge);
      return;
    }
    edges.remove(edge);
    requestEdgeAnalysis();

    ((AbstractRadioMedium.RadioMediumObservable)
        this.getRadioMediumObservable()).setRadioMediumChangedAndNotify();
  }

  public void clearEdges() {
    edges.clear();
    requestEdgeAnalysis();

    ((AbstractRadioMedium.RadioMediumObservable)
        this.getRadioMediumObservable()).setRadioMediumChangedAndNotify();
  }

  public Edge[] getEdges() {
    return edges.toArray(new Edge[0]);
  }

  /**
   * Signal that the configuration changed, and needs to be re-analyzed
   * before used.
   */
  public void requestEdgeAnalysis() {
    edgesDirty = true;
  }

  public boolean needsEdgeAnalysis() {
    return edgesDirty;
  }

  public void unregisterRadioInterface(Radio radio, Simulation sim) {
    super.unregisterRadioInterface(radio, sim);

    for (Edge edge: getEdges()) {
      if (edge.source == radio || edge.superDest.radio == radio) {
        removeEdge(edge);
        requestEdgeAnalysis();
      }
    }
  }

  public void updateSignalStrengths() {

    /* Reset signal strengths */
    for (Radio radio : getRegisteredRadios()) {
      radio.setCurrentSignalStrength(SS_NOTHING);
    }

    /* Set signal strengths */
    RadioConnection[] conns = getActiveConnections();
    for (RadioConnection conn : conns) {
      if (conn.getSource().getCurrentSignalStrength() < SS_STRONG) {
        conn.getSource().setCurrentSignalStrength(SS_STRONG);
      }
      for (Radio dstRadio : conn.getDestinations()) {
        if (dstRadio.getCurrentSignalStrength() < SS_STRONG) {
          dstRadio.setCurrentSignalStrength(SS_STRONG);
        }
      }
    }

    /* Set signal strength to weak on interfered */
    for (RadioConnection conn : conns) {
      for (Radio intfRadio : conn.getInterfered()) {
        if (intfRadio.getCurrentSignalStrength() < SS_STRONG) {
          intfRadio.setCurrentSignalStrength(SS_STRONG);
        }

        if (!intfRadio.isInterfered()) {
          /*logger.warn("Radio was not interfered");*/
          intfRadio.interfereAnyReception();
        }
      }
    }
  }



  /**
   * Generates hash table using current edges for efficient lookup.
   */
  protected void analyzeEdges() {
    Hashtable<Radio,ArrayList<DestinationRadio>> listTable =
      new Hashtable<Radio,ArrayList<DestinationRadio>>();

    /* Fill edge hash table with all edges */
    for (Edge edge: getEdges()) {
      ArrayList<DestinationRadio> destRadios;
      if (!listTable.containsKey(edge.source)) {
        destRadios = new ArrayList<DestinationRadio>();
      } else {
        destRadios = listTable.get(edge.source);
      }

      destRadios.add(edge.superDest);
      listTable.put(edge.source, destRadios);
    }

    /* Convert to arrays */
    Hashtable<Radio,DestinationRadio[]> arrTable =  new Hashtable<Radio,DestinationRadio[]>();
    Enumeration<Radio> sources = listTable.keys();
    while (sources.hasMoreElements()) {
      Radio source = sources.nextElement();
      DestinationRadio[] arr = listTable.get(source).toArray(new DestinationRadio[0]);
      arrTable.put(source, arr);
    }

    this.edgesTable = arrTable;
    edgesDirty = false;
  }

  /**
   * Returns all potential destination radios, i.e. all radios "within reach".
   * Does not consider radio channels, transmission success ratios etc.
   *  
   * @param source Source radio
   * @return All potential destination radios
   */
  public DestinationRadio[] getPotentialDestinations(Radio source) {
    if (edgesDirty) {
      analyzeEdges();
    }
    return edgesTable.get(source);
  }

  public RadioConnection createConnections(Radio source) {
    if (edgesDirty) {
      analyzeEdges();
    }
    if (edgesDirty) {
      logger.fatal("Error when analyzing edges, aborting new radio connection");
      return new RadioConnection(source);
    }

    /* Create new radio connection using edge hash table */
    RadioConnection newConn = new RadioConnection(source);
    DestinationRadio[] destinations = getPotentialDestinations(source);
    if (destinations == null || destinations.length == 0) {
      /* No destinations */
      /*logger.info(sendingRadio + ": No dest");*/
      return newConn;
    }

    /*logger.info(source + ": " + destinations.length + " potential destinations");*/
    for (DestinationRadio d: destinations) {
      DGRMDestinationRadio dest = (DGRMDestinationRadio) d;
      if (dest.radio == source) {
        /* Fail: cannot receive our own transmission */
        /*logger.info(source + ": Fail, receiver is sender");*/
        continue;
      }

      /* Fail if radios are on different (but configured) channels */ 
      if (source.getChannel() >= 0 &&
          dest.radio.getChannel() >= 0 &&
          source.getChannel() != dest.radio.getChannel()) {
        continue;
      }

      if (!dest.radio.isRadioOn()) {
        /* Fail: radio is off */
        /*logger.info(source + ": Fail, off");*/
        newConn.addInterfered(dest.radio);
        continue;
      }

      if (dest.ratio < 1.0 && random.nextDouble() > dest.ratio) {
        /*logger.info(source + ": Fail, randomly");*/
        /* TODO Interfere now? */
        newConn.addInterfered(dest.radio);

        dest.radio.interfereAnyReception();
        RadioConnection otherConnection = null;
        for (RadioConnection conn : getActiveConnections()) {
          for (Radio dstRadio : conn.getDestinations()) {
            if (dstRadio == dest.radio) {
              otherConnection = conn;
              break;
            }
          }
        }
        if (otherConnection != null) {
          otherConnection.addInterfered(dest.radio);
        }
        continue;
      }

      if (dest.radio.isReceiving()) {
        /* Fail: radio is already actively receiving */
        /*logger.info(source + ": Fail, receiving");*/
        newConn.addInterfered(dest.radio);

        /* We will also interfere with the other connection */
        dest.radio.interfereAnyReception();
        RadioConnection otherConnection = null;
        for (RadioConnection conn : getActiveConnections()) {
          for (Radio dstRadio : conn.getDestinations()) {
            if (dstRadio == dest.radio) {
              otherConnection = conn;
              break;
            }
          }
        }
        if (otherConnection != null) {
          otherConnection.addInterfered(dest.radio);
        }
        continue;
      }

      if (dest.radio.isInterfered()) {
        /* Fail: radio is interfered in another connection */
        /*logger.info(source + ": Fail, interfered");*/
        newConn.addInterfered(dest.radio);
        continue;
      }

      /* Success: radio starts receiving */
      /*logger.info(source + ": OK: " + dest.radio);*/
      newConn.addDestination(dest.radio, dest.delay);
    }

    return newConn;
  }

  public Collection<Element> getConfigXML() {
    ArrayList<Element> config = new ArrayList<Element>();
    Element element;

    for (Edge edge: getEdges()) {
      element = new Element("edge");
      element.addContent(edge.getConfigXML());
      config.add(element);
    }

    return config;
  }

  private Collection<Element> delayedConfiguration = null;
  public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
    random = simulation.getRandomGenerator();

    /* Wait until simulation has been loaded */
    delayedConfiguration = configXML;
    return true;
  }
  public void simulationFinishedLoading() {
    if (delayedConfiguration == null) {
      return;
    }

    boolean oldConfig = false;
    for (Element element : delayedConfiguration) {
      if (element.getName().equals("edge")) {
        Collection<Element> edgeConfig = element.getChildren();
        Radio source = null;
        DestinationRadio dest = null;
        for (Element edgeElement : edgeConfig) {
          if (edgeElement.getName().equals("src")) {
            oldConfig = true;

            /* Old config: lookup source mote */
            for (Mote m: simulation.getMotes()) {
              if (m.toString().equals(edgeElement.getText())) {
                logger.info("Old config: mapping '" + edgeElement.getText() + "' to node " + m.getID());
                source = m.getInterfaces().getRadio();
                break;
              }
            }
          } else if (edgeElement.getName().equals("source")) {
            source = simulation.getMoteWithID(
                Integer.parseInt(edgeElement.getText())).getInterfaces().getRadio();
          } else if (oldConfig && edgeElement.getName().equals("ratio")) {
            /* Old config: parse link ratio */
            double ratio = Double.parseDouble(edgeElement.getText());
            ((DGRMDestinationRadio)dest).ratio = ratio;
          } else if (edgeElement.getName().equals("dest")) {
            if (oldConfig) {
              /* Old config: create simple destination link */
              Radio destRadio = null;
              for (Mote m: simulation.getMotes()) {
                if (m.toString().equals(edgeElement.getText())) {
                  logger.info("Old config: mapping '" + edgeElement.getText() + "' to node " + m.getID());
                  destRadio = m.getInterfaces().getRadio();
                  break;
                }
              }
              dest = new DGRMDestinationRadio(destRadio);
            } else {
              String destClassName = edgeElement.getText().trim();
              if (destClassName == null || destClassName.isEmpty()) {
                continue;
              }
              Class<? extends DestinationRadio> destClass =
                simulation.getGUI().tryLoadClass(this, DestinationRadio.class, destClassName);
              if (destClass == null) {
                throw new RuntimeException("Could not load class: " + destClassName);
              }
              try {
                dest = destClass.newInstance();
                dest.setConfigXML(edgeElement.getChildren(), simulation);
              } catch (Exception e) {
                throw (RuntimeException) 
                new RuntimeException("Unknown class: " + destClassName).initCause(e);
              }
            }
          }
        }
        if (source == null || dest == null) {
          logger.fatal("Failed loading DGRM links, aborting");
          return;
        } else {
          addEdge(new Edge(source, dest));
        }
      }
    }
    requestEdgeAnalysis();
    delayedConfiguration = null;
  }

  public static class Edge {
    public Radio source = null;
    public DestinationRadio superDest = null;

    public Edge(Radio source, DestinationRadio dest) {
      this.source = source;
      this.superDest = dest;
    }

    private Collection<Element> getConfigXML() {
      ArrayList<Element> config = new ArrayList<Element>();
      Element element;

      element = new Element("source");
      element.setText("" + source.getMote().getID());
      config.add(element);

      element = new Element("dest");
      element.setText(superDest.getClass().getName());
      Collection<Element> destConfig = superDest.getConfigXML();
      if (destConfig != null) {
        element.addContent(destConfig);
        config.add(element);
      }

      return config;
    }
  }
}
