/**
   * This method does bandwidth allocation. This method should be used only for upload bandwidth
   * managers.
   *
   * @param remoteNodeId
   * @param message
   * @param bytes
   * @throws Exception
   */
  public Transfer sendBytes(int remoteNodeId, Message message, long bytes) throws Exception {
    // System.out.print("Send Bytes: ");

    // the delay is only used as a minimum possible delay in case the bandwidth delay is less that
    // it
    // in other words, the final delay will not be less that 'delay'.
    long delay = 0; // delayManager.getDelay(this.nodeId, remoteNodeId);

    BandwidthManager remoteBandwidthManager = network.get(remoteNodeId).getDownBandwidthManager();

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    double bandwidthInBPS =
        Math.min(getNextTransferBandwidth(), remoteBandwidthManager.getNextTransferBandwidth());

    bandwidthInBPS =
        Math.floor(bandwidthInBPS); // to prevent future rounding errors, prevent fractions

    // ms =  b/(b/ms)
    long timeInMilliSec =
        Math.max((long) UnitConversion.secToMilliSec(bytes / (bandwidthInBPS)), /*delay*/ 0);

    long now = scheduler.now;

    message.setDest(remoteNodeId);
    message.setSource(this.nodeId);
    message.setBytes(bytes);

    MessageDeliveryEvent evt = new MessageDeliveryEvent(now + timeInMilliSec, message);

    Transfer trns = new Transfer(bandwidthInBPS, this, remoteBandwidthManager, bytes, evt);
    message.setTransfer(trns);
    trns.setMessage(message); // for debug purposes only

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    this.makeRoomFor(bandwidthInBPS);
    this.availableInBPS -= bandwidthInBPS;

    remoteBandwidthManager.makeRoomFor(bandwidthInBPS);
    remoteBandwidthManager.availableInBPS -= bandwidthInBPS;

    // this must not be done before we call makeRoomFor,
    // cuz that function depend on the old size of the transfers array List
    // and it depends on the old availableInBPS
    transfers.add(trns);
    remoteBandwidthManager.transfers.add(trns);

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    scheduler.enqueue(evt);

    P.rint(
        "t( "
            + scheduler.now
            + " ) SEND BYTES ("
            + message.getSource()
            + "->"
            + message.getDest()
            + ") "
            + "["
            + bytes
            + " b] , alloc_bw = "
            + bandwidthInBPS
            + " BPS of "
            + totalBandwidthInBPS
            + ", curr_trans = "
            + transfers.size()
            + "/ "
            + remoteBandwidthManager.transfers.size()
            + ", time_est. = "
            + timeInMilliSec
            + " ms, delvr = "
            + evt.getTime()
            + ", "
            + message.toString());

    return trns;
  }
/**
 * This classes is attached to one and only one {@link Node}. It is responsible to manage the
 * bandwidth allocation and deallocation on that node. This classes manages several {@link Transfer}
 * instances. An instance is either a download manager or an upload manager.
 *
 * @author meemo
 */
public class BandwidthManager {

  // the difference from zero of which we consider a number is zero
  static final double BANDWIDTH_TOL = 1;

  private Scheduler scheduler = Scheduler.getInstance();
  private DelayManager delayManager = DelayManager.getInstance();
  private Network network = Network.getInstance();

  public long totalBandwidthInBPS;
  double availableInBPS;
  private int nodeId;

  ArrayList<Transfer> transfers = new ArrayList<Transfer>();

  public BandwidthManager(long BPS, int nodeId) {
    super();
    this.availableInBPS = this.totalBandwidthInBPS = BPS;
    this.nodeId = nodeId;
  }

  private double getNextTransferBandwidth() {
    return Math.max(availableInBPS, totalBandwidthInBPS / (double) (transfers.size() + 1));
  }

  /**
   * This method will modify the existing transfers. It will only modify those who are taking more
   * than their fairshare. It will modify them proportional to their bandwidth, i.e. the more share
   * they have, the more they will be cut.
   *
   * @invariant availableBandwidthInBPS >= bps
   * @param bps the target bandwidth
   * @throws Exception
   */
  private void makeRoomFor(final double bps) throws Exception {
    double fairShare = totalBandwidthInBPS / (double) (transfers.size() + 1);

    // assert less than fair  share
    // A.ssert(bps <=  getNextTransferBandwidth());

    ArrayList<Transfer> trespassingTransfers = new ArrayList<Transfer>();

    sanityCheck();

    // this will only be true if the fair share is larger than the available bandwidth
    if (bps > availableInBPS) // if this is true, then there is trespassing transfers
    {
      final double amountToReclaim = bps - availableInBPS;
      double actualReclaimed = 0;

      double total = 0;
      for (Transfer t : transfers) {
        if (t.bandwidthInBPS > fairShare) {
          trespassingTransfers.add(t);
          total += t.bandwidthInBPS;
        }
      }

      // sort them ascending by bandwidth
      Collections.sort(
          trespassingTransfers,
          new Comparator<Transfer>() {
            public int compare(Transfer o1, Transfer o2) {
              return (int) (o1.bandwidthInBPS - o2.bandwidthInBPS);
            }
          });

      double extra = 0;

      for (Transfer t : trespassingTransfers) {
        double deduction = amountToReclaim * t.bandwidthInBPS / total;

        deduction = Math.ceil(deduction); // to prevent future rounding errors, avoid fractions

        if (t.bandwidthInBPS - deduction < fairShare) {
          double oldDeduction = deduction;
          deduction = t.bandwidthInBPS - fairShare;
          extra += oldDeduction - deduction;
        } else if (t.bandwidthInBPS - deduction > fairShare && extra > 0) {
          // take from extra
          double oldDeduction = deduction;
          double newBW = t.bandwidthInBPS - deduction;
          double diffFromFairShare = newBW - fairShare;
          if (diffFromFairShare > extra) {
            double takeFromExtra = extra;
            deduction += takeFromExtra;
          } else // diff <= extra
          {
            double takeFromExtra = diffFromFairShare;
            deduction += takeFromExtra;
          }
          extra -= oldDeduction - deduction;
        }

        deduction = Math.ceil(deduction);

        actualReclaimed += deduction;

        BandwidthManager remoteBwm = t.getOtherBandwidthManagerThan(this);
        A.ssert(remoteBwm != this);
        // remoteBwm.sanityCheck();

        if (deduction < BANDWIDTH_TOL) continue;

        double newBw = t.bandwidthInBPS - deduction;
        t.setNextEpochBW(newBw);
      }

      HashSet<BandwidthManager> candidates = new HashSet<BandwidthManager>();

      for (Transfer t : trespassingTransfers) {
        if (t.getNextEpochBW() < 0) continue;

        double deducted = t.bandwidthInBPS - t.getNextEpochBW();

        t.applyNextEpochBW();

        BandwidthManager remoteBWM = t.getOtherBandwidthManagerThan(this);

        remoteBWM.availableInBPS += deducted;

        candidates.add(remoteBWM);
      }

      availableInBPS += actualReclaimed;
      A.ssert(availableInBPS > bps || Math.abs(availableInBPS - bps) < BANDWIDTH_TOL);

      // we are going to lie about our available bandwidth cuz we are reserving bps of them.
      // availableInBPS -= bps;
      double oldVal = availableInBPS;
      availableInBPS = 0;
      for (BandwidthManager bwm : candidates) bwm.boostLocalTransfers();
      // availableInBPS += bps;
      availableInBPS = oldVal;
    }

    A.ssert(availableInBPS > bps || Math.abs(availableInBPS - bps) < BANDWIDTH_TOL);
  }

  /*
   * This method is called upon returning bandwidth to availabeInBPS
   * and we want the current transfers to make use of it.
   */
  public void boostLocalTransfers() throws Exception {
    if (transfers.isEmpty()
        || this.nodeId == /*backup source*/ 30
        || this.transfers.size() > 50 /*effectively means that each transfer fair share is < 2% */)
      return;

    // sanityCheck();

    P.rint(this.nodeId + " boosting local transfers");

    ArrayList<Transfer> looseTransfers = new ArrayList<Transfer>();

    for (Transfer t : transfers) {
      t.updatedLooseValue();
      if (Math.floor(t.looseValue) > BANDWIDTH_TOL) {
        looseTransfers.add(t);
      }
    }

    int loops = 0;

    while (!(looseTransfers.isEmpty()
        || availableInBPS < BANDWIDTH_TOL * (looseTransfers.size()))) {
      loops++;

      Set<BandwidthManager> candidates = new HashSet<BandwidthManager>();

      double error = 0;

      for (Transfer t : looseTransfers) {
        double added = t.looseValue;

        added = Math.floor(added); // to prevent future rounding errors, remove fractions

        error += t.looseValue - added;

        if (added < BANDWIDTH_TOL) continue;

        candidates.add(t.getOtherBandwidthManagerThan(this));

        t.setNextEpochBW(t.bandwidthInBPS + added);
      }

      int errorBytes = (int) error;

      for (Transfer t : looseTransfers) {
        A.ssert(t.getNextEpochBW() > 0);

        double added = t.getNextEpochBW() - t.bandwidthInBPS;

        t.applyNextEpochBW();

        t.sourceBandwidthManager.availableInBPS -= added;
        t.destBandwidthManager.availableInBPS -= added;
      }

      ArrayList<Transfer> stillLoose = new ArrayList<Transfer>();

      for (Transfer t : looseTransfers) {
        t.updatedLooseValue();
        if (Math.floor(t.looseValue) > BANDWIDTH_TOL) {
          stillLoose.add(t);
        }
      }

      looseTransfers = stillLoose;

      if (errorBytes < totalBandwidthInBPS * 2.0 / 100) break;
    }

    //		for(Transfer t : transfers)
    //		{
    //			A.ssert(!t.loose());
    //		}
    P.rint("boosting done in " + loops + " loops");
  }

  // FIXME assure this function is transitive...
  double preferredAddTo(Transfer t) {
    final double boostTol = 1; // // don't boost connections below 10 Kbps
    if (this.availableInBPS < BANDWIDTH_TOL
        || Math.min(t.sourceBandwidthManager.availableInBPS, t.destBandwidthManager.availableInBPS)
            < BANDWIDTH_TOL * boostTol) return 0;

    double usedBw = 0; // BPS - availableInBPS;
    int numLoose = 0;
    for (Transfer t1 : transfers)
      if (Math.min(t1.sourceBandwidthManager.availableInBPS, t1.destBandwidthManager.availableInBPS)
          > BANDWIDTH_TOL * boostTol) {
        usedBw += t1.bandwidthInBPS;
        numLoose++;
      }

    double retval = 0;
    if (usedBw > 0) retval = availableInBPS * t.bandwidthInBPS / usedBw;
    else retval = 0; // */

    // double retval = availableInBPS/transfers.size();
    // retval = Math.floor(retval); // to prevent future rounding errors, remove fractions

    return retval;
  }

  //	private double preferredAddTo(Transfer t) {
  //		int numLoose = 0;
  //		for(Transfer t1 : transfers)
  //			if(t1.loose())
  //				numLoose++;
  //		double retval = availableInBPS / numLoose;
  //
  //		retval = Math.floor(retval); // to prevent future rounding errors, remove fractions
  //
  //		return retval;
  //	}

  /**
   * This method does bandwidth allocation. This method should be used only for upload bandwidth
   * managers.
   *
   * @param remoteNodeId
   * @param message
   * @param bytes
   * @throws Exception
   */
  public Transfer sendBytes(int remoteNodeId, Message message, long bytes) throws Exception {
    // System.out.print("Send Bytes: ");

    // the delay is only used as a minimum possible delay in case the bandwidth delay is less that
    // it
    // in other words, the final delay will not be less that 'delay'.
    long delay = 0; // delayManager.getDelay(this.nodeId, remoteNodeId);

    BandwidthManager remoteBandwidthManager = network.get(remoteNodeId).getDownBandwidthManager();

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    double bandwidthInBPS =
        Math.min(getNextTransferBandwidth(), remoteBandwidthManager.getNextTransferBandwidth());

    bandwidthInBPS =
        Math.floor(bandwidthInBPS); // to prevent future rounding errors, prevent fractions

    // ms =  b/(b/ms)
    long timeInMilliSec =
        Math.max((long) UnitConversion.secToMilliSec(bytes / (bandwidthInBPS)), /*delay*/ 0);

    long now = scheduler.now;

    message.setDest(remoteNodeId);
    message.setSource(this.nodeId);
    message.setBytes(bytes);

    MessageDeliveryEvent evt = new MessageDeliveryEvent(now + timeInMilliSec, message);

    Transfer trns = new Transfer(bandwidthInBPS, this, remoteBandwidthManager, bytes, evt);
    message.setTransfer(trns);
    trns.setMessage(message); // for debug purposes only

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    this.makeRoomFor(bandwidthInBPS);
    this.availableInBPS -= bandwidthInBPS;

    remoteBandwidthManager.makeRoomFor(bandwidthInBPS);
    remoteBandwidthManager.availableInBPS -= bandwidthInBPS;

    // this must not be done before we call makeRoomFor,
    // cuz that function depend on the old size of the transfers array List
    // and it depends on the old availableInBPS
    transfers.add(trns);
    remoteBandwidthManager.transfers.add(trns);

    sanityCheck();
    remoteBandwidthManager.sanityCheck();

    scheduler.enqueue(evt);

    P.rint(
        "t( "
            + scheduler.now
            + " ) SEND BYTES ("
            + message.getSource()
            + "->"
            + message.getDest()
            + ") "
            + "["
            + bytes
            + " b] , alloc_bw = "
            + bandwidthInBPS
            + " BPS of "
            + totalBandwidthInBPS
            + ", curr_trans = "
            + transfers.size()
            + "/ "
            + remoteBandwidthManager.transfers.size()
            + ", time_est. = "
            + timeInMilliSec
            + " ms, delvr = "
            + evt.getTime()
            + ", "
            + message.toString());

    return trns;
  }

  /*
   * Make sure that available bandwidth + used bandwidth sum to the original total bandwidth
   */
  private void sanityCheck() throws Exception {
    if (Math.abs(availableInBPS) < BANDWIDTH_TOL / 10) availableInBPS = 0;
    if (Math.abs(availableInBPS - totalBandwidthInBPS) < BANDWIDTH_TOL / 10)
      availableInBPS = totalBandwidthInBPS;
    if (!Conf.BW_SANITY_CHECK) return;

    A.ssert(totalBandwidthInBPS >= -BANDWIDTH_TOL);
    A.ssert(availableInBPS >= -BANDWIDTH_TOL);
    if (totalBandwidthInBPS - availableInBPS < -BANDWIDTH_TOL)
      A.ssert(totalBandwidthInBPS - availableInBPS >= BANDWIDTH_TOL);

    sumUsedSanityCheck();
  }

  private void sumUsedSanityCheck() throws Exception {
    if (!Conf.BW_SANITY_CHECK) return;

    if (transfers.isEmpty()) A.ssert(availableInBPS == totalBandwidthInBPS);

    double usedBandwidth = 0;
    for (Transfer t : transfers) {
      if (t.deliveryEvent.time < scheduler.now) A.ssert(t.deliveryEvent.time >= scheduler.now);
      usedBandwidth += t.bandwidthInBPS;
    }
    if (Math.abs(((usedBandwidth + availableInBPS) - this.totalBandwidthInBPS) / transfers.size())
        > BANDWIDTH_TOL)
      A.ssert(
          Math.abs(((usedBandwidth + availableInBPS) - this.totalBandwidthInBPS))
              < BANDWIDTH_TOL); // equality test
  }

  /**
   * This method deallocated bandwidth This method should be used only for download bandwidth
   * managers.
   *
   * @param transfer
   * @throws Exception
   */
  public void receive(Transfer transfer) throws Exception {

    // can't assert that in fail mode
    // FIXME return the assertion in non-fail mode
    // if(transfer.deliveryEvent.time != scheduler.now)
    //	A.ssert(transfer.deliveryEvent.time == scheduler.now);
    if (transfer.failed) return;

    sanityCheck();
    transfer.getSourceBandwidthManager().sanityCheck();

    this.availableInBPS += transfer.bandwidthInBPS;
    transfers.remove(transfer);

    transfer.getSourceBandwidthManager().availableInBPS += transfer.bandwidthInBPS;
    transfer.getSourceBandwidthManager().transfers.remove(transfer);

    sanityCheck();
    transfer.getSourceBandwidthManager().sanityCheck();

    transfer.sourceBandwidthManager.boostLocalTransfers();
    transfer.destBandwidthManager.boostLocalTransfers();

    sanityCheck();
    transfer.getSourceBandwidthManager().sanityCheck();

    P.rint(
        "t("
            + scheduler.now
            + ") RECV BYTES "
            + transfer.getSourceBandwidthManager().getNodeId()
            + "-->"
            + transfer.getDestBandwidthManager().getNodeId()
            + " <"
            + transfer.getMessage().seqNum
            + "> / "
            + transfer.originalStartTime
            + ", curr_trans = "
            + transfer.getSourceBandwidthManager().transfers.size()
            + ", curr_bw="
            + availableInBPS
            + " out of "
            + totalBandwidthInBPS);
  }

  public int getNodeId() {
    return nodeId;
  }

  public ArrayList<Transfer> getTranfers() {
    return transfers;
  }

  public void fail() throws Exception {
    while (!transfers.isEmpty()) transfers.get(0).fail();
  }
}