/*-
 * Copyright (c) 2004 INRIA
 * Copyright (c) 2002-2005 Sam Leffler, Errno Consulting
 * 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,
 without modification.
 * 2. Redistributions in binary form must reproduce at minimum a disclaimer
 *    similar to the "NO WARRANTY" disclaimer below ("Disclaimer") and any
 *    redistribution must be conditioned upon including a substantially
 *    similar Disclaimer requirement for further binary redistribution.
 * 3. Neither the names of the above-listed copyright holders nor the names
 *    of any contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * Alternatively, this software may be distributed under the terms of the
 * GNU General Public License ("GPL") version 2 as published by the Free
 * Software Foundation.
 *
 * NO WARRANTY
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF NONINFRINGEMENT, MERCHANTIBILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES.
 *
 * $Id: amrr.h 2394 2007-05-30 01:41:18Z mtaylor $
 */
/**
 * 
 */
package jist.swans.rate;

import java.util.Enumeration;
import java.util.Hashtable;

import org.apache.log4j.Logger;

import jist.runtime.JistAPI;
import jist.runtime.Main;
import jist.swans.Constants;
import jist.swans.mac.MacAddress;
import jist.swans.mac.MacInfo;
import jist.swans.misc.Message;
import jist.swans.misc.MessageAnno;
import jist.swans.misc.Util;

/**
 * @author ofriedri
 */
public class AmrrHighLat extends AbstractRcaHighLatency {

  /** AMRR logger. */
  public static final Logger log = Logger.getLogger(AmrrHighLat.class);

  /** rate control update interval (ms) */
  public static final int rateInterval = 1000;

  public static final int maxSuccessThreshold = 10;

  public static final int minSuccessThreshold = 1;

  private Hashtable /* MacAddress, AmrrState */m_macToState;

  private RateControlUpdateRunner updater;

  /**
   * @param macInfo
   */
  public AmrrHighLat(MacInfo macInfo) {
    super(macInfo);
    setRetryLimits();
    m_macToState = new Hashtable();
    m_mrrc = new MultiRateRetryChain(AmrrState.size);
    updater = new RateControlUpdateRunner(this);
    JistAPI.runAt(updater, 1 * Constants.SECOND);
    if (log.isDebugEnabled())
      log.debug("MacInfo: " + (Object) m_macInfo);
  }

  /*
   * (non-Javadoc)
   * 
   * @see jist.swans.rate.RateControlAlgorithmIF#getNextDataRate(jist.swans.mac.MacAddress,
   *      int, int)
   */
  public int getNextDataRate(Message packet, MessageAnno anno, Object nextHop,
      int frameLen, int triesBefore) {
    // If this is not the first try, use the saved mrrc to return the next rate
    if (triesBefore > 0)
      return super
          .getNextDataRate(packet, anno, nextHop, frameLen, triesBefore);

    AmrrState state = getAmrrState((MacAddress) nextHop);
    for (int i = 0; i < AmrrState.size; i++) {
      if (Main.ASSERT)
        Util.assertion(state.txRates[i] != Constants.BANDWIDTH_INVALID
            && state.txTries[i] >= 0);
      m_mrrc.bitrates[i] = state.txRates[i];
      m_mrrc.tries[i] = state.txTries[i];
    }
    if (log.isDebugEnabled())
      log.debug("txRates to " + (MacAddress) nextHop + ": " + m_mrrc);
    return m_mrrc.bitrates[0];
  }

  /*
   * (non-Javadoc)
   * 
   * @see jist.swans.rate.RateControlAlgorithmIF#reportPacketTx(jist.swans.mac.MacAddress,
   *      int, int, int, int, int, int)
   */
  public void reportPacketTx(MacAddress nextHop, int type, int size,
      int bitrate, int shortTries, int longTries, int status) {
    int tries = shortTries + longTries; // at least one of them is 0
    AmrrState state = getAmrrState(nextHop);
    state.txTriesCnt[0]++;
    if (tries > 1) {
      state.txTriesCnt[1]++;
      if (tries > 2) {
        state.txTriesCnt[2]++;
        if (tries > 3) {
          state.txTriesCnt[3]++;
          if (status != jist.swans.Constants.RADIO_RECV_STATUS_OK.byteValue())
            state.txFailureCnt++;
        }
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see jist.swans.rate.RateControlAlgorithmIF#getRetryLimit(int)
   */
  public byte getRetryLimit(MacAddress nextHop, int packetLength) {
    return 3; // total transmission tries = 4
  }

  /**
   * Returns an AmrrState object for the given MAC address. AmrrState objects
   * are created as they are needed.
   * 
   * @param address The MAC address for which to retrieve the current state of
   *          the AMRR algorithm
   * @return the state of the AMRR algorithm for the given MAC address
   */
  protected AmrrState getAmrrState(MacAddress address) {
    AmrrState as = (AmrrState) m_macToState.get(address);
    if (as == null) {
      if (s_macRateInfo.getMacInfo(address) == null)
        throw new RuntimeException("Why don't I have a MacInfo for " + address
            + "?");
      as = new AmrrState(minSuccessThreshold);
      m_macToState.put(address, as);
      rateInit(address);
    }
    return as;
  }

  /**
   * Set the initial bit-rate and rate/try pairs.
   */
  private void rateInit(MacAddress address) {
    int[] bitrates = s_macRateInfo.getBitrates(address);
    int rateIdx = bitrates.length - 1;
    if (bitrates[rateIdx] > Constants.BANDWIDTH_11Mbps) {
      while (rateIdx >= 0 && bitrates[rateIdx] > Constants.BANDWIDTH_36Mbps)
        rateIdx--;
      if (Main.ASSERT)
        Util.assertion(rateIdx >= 0);
    }
    rateUpdate(address, rateIdx);
  }

  /**
   * Update the internal state with txRates and txTries to use for next
   * transmission.
   * 
   * @param rateIdx Bit-rate index for the first tx try (of the next packet)
   *          TODO Check for better solution of not using
   *          Constants.BANDWIDTH_INVALID.
   */
  private void rateUpdate(MacAddress address, int rateIdx) {
    AmrrState state = getAmrrState(address);
    int[] bitrates = s_macRateInfo.getBitrates(address);
    state.txRates[0] = bitrates[rateIdx];
    for (int i = 0; i < AmrrState.size; i++)
      state.txTries[i] = 1;
    if (--rateIdx >= 0)
      state.txRates[1] = bitrates[rateIdx];
    else
      state.txRates[1] = bitrates[0]; // Constants.BANDWIDTH_INVALID;
    if (--rateIdx >= 0)
      state.txRates[2] = bitrates[rateIdx];
    else
      state.txRates[2] = bitrates[0]; // Constants.BANDWIDTH_INVALID;
    // Only go here, if the lowest bitrate hasn't been reached yet
    if (rateIdx > 0)
      state.txRates[3] = bitrates[0];
    else
      state.txRates[3] = bitrates[0]; // Constants.BANDWIDTH_INVALID;
  }

  /**
   * Checks the state of the algorithm for a specific (receiver) MAC address and
   * updates it accordingly.
   * 
   * @param address MAC address for which to adjust the current algo state
   */
  private void rateControl(MacAddress address) {
    AmrrState state = getAmrrState(address);
    int[] bitrates = s_macRateInfo.getBitrates(address);
    int currRateIdx = Util.isInArray(bitrates, m_mrrc.bitrates[0]);
    int oldRateIdx = currRateIdx;

    if (log.isDebugEnabled())
      log.debug("[t=" + Util.getTimeDotted() + "] state for " + address + ": "
          + state);

    if (state.isSuccess() && state.isEnough()) {
      state.success++;
      if (state.success == state.successThreshold
          && !(oldRateIdx + 1 >= bitrates.length)) {
        state.recovery = true;
        state.success = 0;
        currRateIdx++;
      } else {
        state.recovery = false;
      }
    } else if (state.isFailure()) {
      state.success = 0;
      if (!(oldRateIdx == 0)) {
        if (state.recovery) {
          /* recovery failure */
          state.successThreshold = Math.min(state.successThreshold * 2,
              maxSuccessThreshold);
          if (log.isDebugEnabled())
            log.debug("increase rate recovery threshold to: "
                + state.successThreshold);
        } else {
          /* simple failure */
          state.successThreshold = minSuccessThreshold;
          if (log.isDebugEnabled())
            log.debug("decrease rate normal threshold to: "
                + state.successThreshold);
        }
        currRateIdx--;
      }
      state.recovery = false;
    }
    if (state.isEnough() || oldRateIdx != currRateIdx) {
      /* reset counters */
      for (int i = 0; i < AmrrState.size; i++)
        state.txTriesCnt[i] = 0;
      state.txFailureCnt = 0;
    }
    if (oldRateIdx != currRateIdx)
      rateUpdate(address, currRateIdx);
  }

  private void timerRateControl() {
    if (log.isDebugEnabled())
      log.debug(Util.getTimeDotted() + " ### running statistics ###");
    Enumeration e = m_macToState.keys();
    while (e.hasMoreElements()) {
      rateControl((MacAddress) e.nextElement());
    }
    // run again in 1 second; can't use sleep(), because this is not a JiST entity
    JistAPI.runAt(updater, JistAPI.getTime() + 1 * Constants.SECOND);
  }

  /**
   * Set the MacInfo object of this RCA to the given one and adapt its retry
   * limits to 3. AMRR sends every packet at most 4 times.
   * 
   * @param macInfo
   */
  public void setRetryLimits() {
    m_macInfo.setRetryLimitShort((byte) 3);
    m_macInfo.setRetryLimitLong((byte) 3);
    if (log.isDebugEnabled())
      log.debug("Both retry limits set to 3");
  }

  /**
   * Small runnable class to work around not being able to use sleep() inside
   * the RCA module (no entities!). So we just schedule this runnable for when
   * we want it to run.
   * 
   * @author ofriedri
   */
  public static class RateControlUpdateRunner implements Runnable {

    private AmrrHighLat amrr;

    public RateControlUpdateRunner(AmrrHighLat a) {
      amrr = a;
    }

    public void run() {
      if (amrr != null)
        amrr.timerRateControl();
    }
  }

}
