//////////////////////////////////////////////////
// JIST (Java In Simulation Time) Project
// Timestamp: <RadioNoise.java Tue 2004/04/13 18:22:55 barr glenlivet.cs.cornell.edu>
//

// Copyright (C) 2004 by Cornell University
// All rights reserved.
// Refer to LICENSE for terms and conditions of use.

package jist.swans.radio;

import jist.swans.field.FieldInterface;
import jist.swans.mac.MacInterface;
import jist.swans.misc.Message;
import jist.swans.misc.MessageAnno;
import jist.swans.Constants;
import jist.swans.phy.PhyMessage;

import jist.runtime.JistAPI;
import jist.runtime.Util;
import jist.runtime.Main;

import java.util.Map;
import java.util.HashMap;

import org.apache.log4j.Logger;

/**
 * <code>RadioNoise</code> is an abstract class which implements some functionality
 * that is common to the independent and additive radio noise simulation models.
 *
 * The following modifications were made in order to support multiple RF channels:
 * - the dynamic part like mode, signalBuffer, ... are swapped out in a separate class {@link State}.
 * - for each available RFChannel there is a {@link State} instance.
 * - the collection of all {@link State} instances is kept in {@link #states}
 * - a message which needs to be transmitted is always sent on the RF channel represented by {@link #currentState}.
 *
 * @author Rimon Barr &lt;barr+jist@cs.cornell.edu&rt;
 * @author Zubow
 * @version $Id: RadioNoise.java,v 1.28 2004/11/05 03:00:59 barr Exp $
 * @since SWANS1.0
 */
public abstract class RadioNoise extends AbstractRadio implements RadioInterface
{

  /** logger for mac events. */
  public static final Logger log = Logger.getLogger(RadioNoise.class.getName());

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

  // properties

  /** radio properties. */
  protected RadioInfo radioInfo;

  /** whether to use and generate annotations. */
  protected boolean useAnnotations;

  /**
   * To support multiple RF channels each available RF channel is represented by an instance of this class.
   */
  public class State implements JistAPI.Timeless {

    /** radio mode: IDLE, SENSING, RECEIVING, SENDING, SLEEP. */
    public byte mode;

    /** message being received. */
    public Message signalBuffer;

    /** only use a minimal set of annos until the packet is finally received! */
    public MessageAnno staticAnnos;

    /** end of transmission time. */
    public long signalFinish;

    /** tx duration time. */
    public long signalDuration;

    /** transmission signal strength. */
    public double signalPower_mW;

    /** number of signals being received. */
    public int signals;

    /** The used RF channel. */
    public RFChannel rfChannel = RFChannel.DEFAULT_RF_CHANNEL;

    protected State() {
      mode = Constants.RADIO_MODE_IDLE;
      unlockSignal();
      signals = 0;

      // create static annos and set receive status correctly
      staticAnnos = new MessageAnno();
      staticAnnos.put(MessageAnno.ANNO_MAC_RF_CHANNEL, RadioInterface.DEFAULT_RF_CHANNEL);
      staticAnnos.put(MessageAnno.ANNO_RADIO_RECV_STATUS, Constants.RADIO_RECV_STATUS_OK);
//      staticAnnos.put(MessageAnno.ANNO_RADIO_NOISE_LEVEL, Double.valueOf(radioInfo.getBackground_mW()));
    }

    protected State(RFChannel channel) {
      this();
      this.rfChannel = channel;

      // create static annos and set receive status correctly
      staticAnnos = new MessageAnno();
      staticAnnos.put(MessageAnno.ANNO_MAC_RF_CHANNEL, channel);
      staticAnnos.put(MessageAnno.ANNO_RADIO_RECV_STATUS, Constants.RADIO_RECV_STATUS_OK);
//      staticAnnos.put(MessageAnno.ANNO_RADIO_NOISE_LEVEL, Double.valueOf(radioInfo.getBackground_mW()));
    }

    /**
     * Lock onto current packet signal.
     *
     * @param msg packet currently on the air
     * @param power_mW signal power (units: mW)
     * @param duration time to EOT (units: simtime)
     */
    protected void lockSignal(Message msg, double power_mW, long duration)
    {
      signalBuffer = msg;
      signalPower_mW = power_mW;
      signalFinish = JistAPI.getTime() + duration;
      signalDuration = duration;
    }

    /**
     * Unlock from current packet signal.
     */
    protected void unlockSignal()
    {
      signalBuffer = null;
      signalPower_mW = 0;
      signalFinish = -1;
      signalDuration = -1;
    }

    public void setMode(byte mode) {
      this.mode = mode;
    }

    public byte getMode() {
      return mode;
    }

    public void setSleepMode(boolean sleep)
    {
      setMode(sleep ? Constants.RADIO_MODE_SLEEP : Constants.RADIO_MODE_IDLE);
    }
  }


  //////////////////////////////////////////////////
  // channel stuff
  //

  /** array per RF channel */
  protected Map states;

  /** Represents the current RF channel the radio is operating on. */
  protected State currentState;

  /**
   * Indicates whether we are in the process of switching the RF channel. In the process of switching the channel
   * the node is deaf.
   */
  protected boolean processingChannelSwitch;

  // entity hookup

  /** field entity downcall reference. */
  protected FieldInterface fieldEntity;

  /** self-referencing radio entity reference. */
  protected RadioInterface self;

  /** mac entity upcall reference. */
  protected MacInterface macEntity;

  protected boolean doCarrierSensing;

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

  /**
   * Create a new radio.
   *
   * @param radioInfo radio properties
   */
  protected RadioNoise(RadioInfo radioInfo)
  {
    this.radioInfo = radioInfo;
    useAnnotations = false;
    this.states = new HashMap();
    this.doCarrierSensing = true;
    this.self = (RadioInterface)JistAPI.proxy(new RadioInterface.Dlg(this),
        RadioInterface.class);
  }

  //////////////////////////////////////////////////
  // entity hookups
  //

  /**
   * Return self-referencing radio entity reference.
   *
   * @return self-referencing radio entity reference
   */
  public RadioInterface getProxy()
  {
    return this.self;
  }

  /**
   * Set upcall field entity reference.
   *
   * @param fieldEntity upcall field entity reference
   */
  public void setFieldEntity(FieldInterface fieldEntity)
  {
    if(!JistAPI.isEntity(fieldEntity)) throw new IllegalArgumentException("entity expected");
    this.fieldEntity = fieldEntity;
  }

  /**
   * Set downcall mac entity reference.
   *
   * @param macEntity downcall mac entity reference
   */
  public void setMacEntity(MacInterface macEntity)
  {
    if(!JistAPI.isEntity(macEntity)) throw new IllegalArgumentException("entity expected");
    this.macEntity = macEntity;
  }

  public boolean isDoCarrierSensing() {
    return doCarrierSensing;
  }

  public void setDoCarrierSensing(boolean doCarrierSensing) {
    this.doCarrierSensing = doCarrierSensing;
  }

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

  public String toString() {
    return radioInfo.getIdInteger().toString();
  }

  /**
   * Return radio properties.
   *
   * @return radio properties
   */
  public RadioInfo getRadioInfo()
  {
    return radioInfo;
  }

  /**
   * Set radio mode. Also notifies mac entity.
   *
   * @param mode radio mode
   */
  public final void setMode(State state, byte mode)
  {
    /*
     * Note: It is important that we always update the mode on the radio even
     * if new_mode == old_mode. Otherwise,
     * after a channel switch the radio mode on the MAC will not updated.
     */
    state.setMode(mode);
    
    // When we are in the right state inform the mac about the state change in the radio.
    if (doCarrierSensing && currentState.equals(state))
      this.macEntity.setRadioMode(mode);
  }

  /**
   * Turn radio off (sleep) or on.
   *
   * @param sleep whether to turn off radio
   */
  public void setSleepMode(boolean sleep)
  {
    setSleepMode(currentState, sleep);
  }

  public void setSleepMode(State state, boolean sleep)
  {
    state.setSleepMode(sleep);
  }

  public boolean isUseAnnotations()
  {
    return useAnnotations;
  }

  public void setUseAnnotations(boolean useAnnotations)
  {
    this.useAnnotations = useAnnotations;
  }

  //////////////////////////////////////////////////
  // signal acquisition
  //

  /**
   * Lock onto current packet signal.
   *
   * @param msg packet currently on the air
   * @param power_mW signal power (units: mW)
   * @param duration time to EOT (units: simtime)
   */
  protected void lockSignal(State state, Message msg, double power_mW, long duration)
  {
    // call the appropriate method
    state.lockSignal(msg, power_mW, duration);
    if (currentState.equals(state)
        && state.mode == Constants.RADIO_MODE_RECEIVING) {
      // peek the MAC only if we are on the right channel (currentState == state)
      // and in receiving mode
      this.macEntity.peek(state.signalBuffer, (useAnnotations ? state.staticAnnos : null));
    } else {
      // this is a regular behavior: either wrong channel or wrong radio mode
      if (log.isDebugEnabled())
        log.debug(radioInfo.getId() + "(" + JistAPI.getTime() +
            " Received on wrong channel [" + state.rfChannel.channel
            + "; curr:" + currentState.rfChannel.channel + "] or in wrong mode"
            + " [" + state.mode + "] (node may be deaf; MAC::peek() not called.)");
    }
  }

  /**
   * Unlock from current packet signal.
   */
  protected void unlockSignal(State state)
  {
    state.unlockSignal();
  }

  //////////////////////////////////////////////////
  // transmission
  //

  // RadioInterface interface
  /** {@inheritDoc} */
  public final void transmit(Message msg, long delay, long duration, MessageAnno anno) {
    // Constants.TX_RX_TURNAROUND
    this.transmit(msg, anno, delay, duration, 0);
  }
  
  /*
   * (non-Javadoc)
   * @see jist.swans.radio.RadioInterface#transmit(jist.swans.misc.Message, jist.swans.misc.MessageAnno, long, long, long)
   */
  public final void transmit(Message msg, MessageAnno anno, 
      long predelay, long duration, long postdelay) {
    /**
     * Messages are always transmitted on the current active RF channel 
     *   (represented by {@link currentState}).
     * To switch the current RF channel used by this radio it is required 
     * to previously call {@link #setChannel()};
     */
    transmit(currentState, msg, anno, predelay, duration, postdelay);
  }

  /**
   * Transmit the given message on the RF channel represented by @param state.
   * @param state use this state to transmit the given frame.
   * @see #transmit(jist.swans.misc.Message, long, long, jist.swans.misc.MessageAnno)
   */
  protected void transmit(State state, Message msg, MessageAnno anno,
      long predelay, long duration, long postdelay)
  {
    // make sure that the current RF channel of the radio is equally to the channel of the phy message.
    if (Main.ASSERT)
      if (msg instanceof PhyMessage)
        if (!((PhyMessage)msg).getRfChannel().equals(currentState.rfChannel))
            throw new RuntimeException("packet is annotated with a wrong RF channel: "
                + ((PhyMessage)msg).getRfChannel() + " vs. " + currentState.rfChannel);

    // transmitting during a channel switch is prohibited. 
    if (processingChannelSwitch)
      throw new RuntimeException(" transmitting during a channel switch is prohibited.");

    // radio in sleep mode
    if (state.mode == Constants.RADIO_MODE_SLEEP)
      return;

    // ensure not currently transmitting
    Util.assertion(state.mode != Constants.RADIO_MODE_TRANSMITTING);

    // If we are currently receiving drop that frame. 
    if (state.mode == Constants.RADIO_MODE_RECEIVING && radioDropEvent.isActive())
      radioDropEvent.handle("transmit", radioInfo, state.signalBuffer,
          state.staticAnnos, state.signalPower_mW);
    // clear receive buffer
    state.signalBuffer = null;
    // use default delay, if necessary
    if(predelay == Constants.RADIO_NOUSER_DELAY)
      predelay = Constants.RX_TX_TURNAROUND__802_11bg;

    // set mode to transmitting
    setMode(state, Constants.RADIO_MODE_TRANSMITTING);

    // schedule message propagation delay (--> RX-TX switching delay)
    JistAPI.sleep(predelay);
    if (radioSendEvent.isActive())
      radioSendEvent.handle(radioInfo, msg, anno, duration);

    if (log.isDebugEnabled())
      log.debug(radioInfo.getId() + "(" + JistAPI.getTime() +
          "): Transmit ... " + (predelay + duration));

    /** deliver message to the field model. */
    fieldEntity.transmit(radioInfo, msg, duration);

    // schedule end of transmission (airtime of the message)
    // mk: added tx/rx turnaround delay
    JistAPI.sleep(duration + postdelay);
    // schedule the related endXXX function.
    self.endTransmit();
  }

  // RadioInterface interface
  /** {@inheritDoc} */
  public final void endTransmit() {
    /** call the internal endTransmit() method with the current state as argument. */
    endTransmit(currentState);
  }

  /**
   * Internal helper method.
   * @param state use this state.
   * @see #endTransmit()
   */
  protected void endTransmit(State state)
  {
    if (log.isDebugEnabled())
      log.debug(radioInfo.getId() + "(" + JistAPI.getTime() + "): endTransmit ... ");

    // radio in sleep mode
    if(state.mode == Constants.RADIO_MODE_SLEEP)
      return;
    // check that we are currently transmitting
    if(state.mode != Constants.RADIO_MODE_TRANSMITTING)
      throw new RuntimeException("radio is not transmitting");

    /** set the mode to sensing if there are ongoing message receptions, otherwise to idle. */
    setMode(state, state.signals>0 ? Constants.RADIO_MODE_SENSING : Constants.RADIO_MODE_IDLE);
  }

  // ////////////////////////////////////////////////
  // channel logic
  //

  /**
   * @inheritDoc
   */
  public void setChannel(RFChannel channel, long delay) {
    /** Check if this is a known RF channel (must be represented by a state). */
    if (states.get(channel) == null)
      throw new IllegalArgumentException("RF channel " + channel + " not available.");

    // radio in sleep mode
    if (currentState.mode == Constants.RADIO_MODE_SLEEP)
      return;

    /** this may happen only at build time and not at runtime (sim time = 0; e.g. setting home channel). */
    if (channel.equals(currentState.rfChannel) && delay > 0)
      throw new RuntimeException("We are already on the right channel; no switching required.");


    /** We are trying to set the chanel while transmitting. This is an error. */
    if (currentState.getMode() == Constants.RADIO_MODE_TRANSMITTING)
      throw new RuntimeException("Unable to switch channel during transmitting");

    /** We are already in the channel switching process. */
    if (processingChannelSwitch)
      throw new RuntimeException("Unable to serve a nested channel switch attempt; we are already switching.");

    /** If we are currently in the process of receiving a packet we have to discard it. */
    dropPacket((State)states.get(channel), "switch");

    /** mark the radio as switching the channel */
    processingChannelSwitch = true;

    if (radioChannelStartSwitchEvent.isActive())
      radioChannelStartSwitchEvent.handle(currentState.rfChannel, RFChannel.INVALID_RF_CHANNEL, delay);

    /** Schedule the end of the channel switching after the given delay. */
    if (log.isDebugEnabled())
      log.debug(radioInfo.getId() + "(" + JistAPI.getTime() + "): Started channel switch from " +
            currentState.rfChannel + " to " + channel);

    /** mark the node as beeing deaf for the time of {@link delay} = channel switching delay */
    JistAPI.sleep(delay);
    // schedule the related endXXX function.
    self.endSetChannel(channel);
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.radio.RadioInterface#endSetChannel(jist.swans.radio.RadioInterface.RFChannel)
   */
  public void endSetChannel(RFChannel channel) {

    /** we finished the channel switching. */
    if(!processingChannelSwitch)
      throw new RuntimeException("radio has to be marked as switching");

    Util.assertion(states.get(channel) != null);
    /** we finished the switching to the new RF channel; now we have to set the current state accordingly. */
    currentState = (State)states.get(channel);

    /** finished channel switching */
    processingChannelSwitch = false;

    if (radioChannelFinishSwitchEvent.isActive())
      radioChannelFinishSwitchEvent.handle(RFChannel.INVALID_RF_CHANNEL, channel);

    if (log.isDebugEnabled())
      log.debug(radioInfo.getId() + "(" + JistAPI.getTime() + "): Channel switch to "
          + channel + " complete");

    // set the radio mode accordingly
    setMode(currentState, currentState.signals > 0 ? Constants.RADIO_MODE_SENSING : Constants.RADIO_MODE_IDLE);

//    byte mode = currentState.signals > 0 ? Constants.RADIO_MODE_SENSING : Constants.RADIO_MODE_IDLE;
//    currentState.setMode(mode);

    /* When we are in the right state inform the mac about the state change in the radio. */
//    this.macEntity.setRadioMode(mode);
  }

  /*
   * (non-Javadoc)
   * @see jist.swans.radio.RadioInterface#getChannel()
   */
  public final RFChannel getChannel() throws JistAPI.Continuation {
    /**
     * Returns the RF channel used by the current state.
     * Note: the RF channel of the radio and the MAC should be in sync!!!
     * */
    return currentState.rfChannel;
  }

  public final void setCurrentState(RFChannel rfChannel) {
    this.currentState = (State)states.get(rfChannel);
  }

  /**
   * If we are currently in the process of receiving a packet we have to discard it.
   * @param reason The reason for the packet drop; here <switch>
   */
  protected void dropPacket(State state, String reason) {
    if (null != currentState.signalBuffer) {

      if (radioDropEvent.isActive())
        radioDropEvent.handle(reason, radioInfo, currentState.signalBuffer,
                currentState.staticAnnos, currentState.signalDuration);

      unlockSignal(currentState);
    }
  }

  /**
   * This method is used to hand over packets to the MAC. Here the rf channel of
   * the current state is compared to the given one to ensure that only readios
   * operating on the right rf  channel will receive that frame.
   *
   * @param state the receiving frame is annotated with a rf channel. This state
   * corresponds to the this rf channel.
   */
  protected void sendToMac(State state, Double powerObj_mW, MessageAnno signalAnno) {
    if (isUsingRightRFChannel(state)) {
      if (useAnnotations) {
        if (null == signalAnno)
          signalAnno = (MessageAnno) state.staticAnnos.clone();
        signalAnno.put(MessageAnno.ANNO_RADIO_SIGNAL_POWER, powerObj_mW);
      } else {
        signalAnno = null;
      }
      macEntity.receive(state.signalBuffer, signalAnno);
    } else {
      // drop due to channel deafness or wrong channel
    }
  }

  /**
   * Checks whether the radio is operating on the right rf channel and not currently
   * in the process of switching the channel.
   * @param state the current state
   * @return true or false
   */
  protected boolean isUsingRightRFChannel(State state) {
    return !processingChannelSwitch && currentState.rfChannel.equals(state.rfChannel);
  }

  /**
   * Checks whether the radio is operating on the wrong rf channel or currently in the channel
   * switching process
   * @param state the current state
   * @return true if deaf; otherwise false
   */
  protected boolean isRFChannelDeaf(State state) {
    return processingChannelSwitch || !currentState.rfChannel.equals(state.rfChannel);
  }

} // class: RadioNoise

