/**
   * 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);
  }
  /**
   * 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 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;
  }