/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.gobblin.util.request_allocation;

import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;


/**
 * A concurrent bounded priority {@link Iterable}. Given a {@link ResourcePool}, a {@link ResourceEstimator}, and a
 * {@link Comparator} as a prioritizer, user can attempt to add elements to this container.
 * The container stores a subset of the offered elements such that their {@link ResourceRequirement} is within the
 * bounds of the {@link ResourcePool}, preferentially storing high priority elements.
 * This functionality is achieved by automatic eviction of low priority items when space for a higher priority item
 * is needed.
 * A call to {@link #iterator()} returns an iterator over the current elements in the container in order from high
 * priority to low priority. Note after calling {@link #iterator()}, no more requests can be added (will throw
 * {@link RuntimeException}).
 *
 * Note: as with a priority queue, e1 < e2 means that e1 is higher priority than e2.
 */
@Slf4j
public class ConcurrentBoundedPriorityIterable<T> implements Iterable<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> {

  private final TreeSet<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> elements;
  private final int dimensions;
  @Getter
  private final Comparator<? super T> comparator;
  private final Comparator<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> allDifferentComparator;
  private final ResourceEstimator<T> estimator;
  private final ResourcePool resourcePool;
  private final ResourceRequirement currentRequirement;
  private volatile boolean rejectedElement = false;
  private volatile boolean closed = false;

  // These are just for statistics
  private final ResourceRequirement maxResourceRequirement;
  private int requestsOffered = 0;
  private int requestsRefused = 0;
  private int requestsEvicted = 0;

  //These are for submitting alertable events
  private String storeRejectedRequestsSetting;
  @Getter
  private List<T> requestsExceedingAvailableResourcePool = Lists.newArrayList();
  @Getter
  private List<T> requestsRejectedWithLowPriority = Lists.newArrayList();
  @Getter
  private List<T> requestsRejectedDueToInsufficientEviction = Lists.newArrayList();
  @Getter
  private List<T> requestsDropped = Lists.newArrayList();

  // These are ResourceRequirements for temporary use to avoid instantiation costs
  private final ResourceRequirement candidateRequirement;
  private final ResourceRequirement tmpRequirement;
  private final ResourceRequirement reuse;

  public ConcurrentBoundedPriorityIterable(final Comparator<? super T> prioritizer,
      ResourceEstimator<T> resourceEstimator, String storeRejectedRequestsSetting, ResourcePool pool) {

    this.estimator = resourceEstimator;
    this.resourcePool = pool;
    this.dimensions = this.resourcePool.getNumDimensions();
    this.comparator = prioritizer;
    this.allDifferentComparator = new AllDifferentComparator();
    this.elements = new TreeSet<>(this.allDifferentComparator);

    this.storeRejectedRequestsSetting = storeRejectedRequestsSetting;

    this.currentRequirement = this.resourcePool.getResourceRequirementBuilder().zero().build();
    this.maxResourceRequirement = new ResourceRequirement(this.currentRequirement);

    this.candidateRequirement = new ResourceRequirement(this.currentRequirement);
    this.tmpRequirement = new ResourceRequirement(this.currentRequirement);
    this.reuse = new ResourceRequirement(this.currentRequirement);
  }

  /**
   * This is the actual {@link Comparator} used in the {@link TreeSet}. Since {@link TreeSet}s use the provided
   * {@link Comparator} to determine equality, we must force elements with the same priority to be different according
   * to the {@link TreeSet}.
   */
  private class AllDifferentComparator implements Comparator<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> {
    @Override
    public int compare(AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> t1,
        AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> t2) {
      int providedComparison = ConcurrentBoundedPriorityIterable.this.comparator.compare(t1.getT(), t2.getT());
      if (providedComparison != 0) {
        return providedComparison;
      }
      return Long.compare(t1.getId(), t2.getId());
    }
  }

  /**
   * Offer an element to the container.
   * @return true if the element was added, false if there was no space and we could not evict any elements to make it fit.
   *         Note that the element may get evicted by future offers, so a return of true is not a guarantee that the
   *         element will be present at any time in the future.
   */
  public boolean add(T t) {
    if (this.closed) {
      throw new RuntimeException(
          ConcurrentBoundedPriorityIterable.class.getSimpleName() + " is no longer accepting requests!");
    }

    AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> newElement =
        new AllocatedRequestsIteratorBase.RequestWithResourceRequirement<>(t,
            this.estimator.estimateRequirement(t, this.resourcePool));
    boolean addedWorkunits = addImpl(newElement);
    if (!addedWorkunits) {
      this.rejectedElement = true;
    }
    return addedWorkunits;
  }

  private synchronized boolean addImpl(AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> newElement) {

    this.maxResourceRequirement.entryWiseMax(newElement.getResourceRequirement());
    this.requestsOffered++;

    if (this.resourcePool.exceedsHardBound(newElement.getResourceRequirement(), false)) {
      // item does not fit even in empty pool
      log.warn(String.format("Request %s is larger than the available resource pool. If the pool is not expanded, "
              + "it will never be selected. Request: %s.", newElement.getT(),
          this.resourcePool.stringifyRequirement(newElement.getResourceRequirement())));
      if (!this.storeRejectedRequestsSetting
          .equalsIgnoreCase(RequestAllocatorConfig.StoreRejectedRequestsConfig.NONE.name())) {
        this.requestsExceedingAvailableResourcePool.add(newElement.getT());
      }
      this.requestsRefused++;
      return false;
    }

    ResourceRequirement candidateRequirement = ResourceRequirement
        .add(this.currentRequirement, newElement.getResourceRequirement(), this.candidateRequirement);

    if (this.resourcePool.exceedsHardBound(candidateRequirement, false)) {

      if (this.comparator.compare(this.elements.last().getT(), newElement.getT()) <= 0) {
        log.debug(
            "Request {} does not fit in resource pool and is lower priority than current lowest priority request. "
                + "Rejecting", newElement.getT());
        this.requestsRefused++;
        if (this.storeRejectedRequestsSetting
            .equalsIgnoreCase(RequestAllocatorConfig.StoreRejectedRequestsConfig.ALL.name())) {
          this.requestsRejectedWithLowPriority.add(newElement.getT());
        }
        return false;
      }

      List<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> toDrop = Lists.newArrayList();

      this.currentRequirement.copyInto(this.tmpRequirement);

      for (AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> dropCandidate : this.elements
          .descendingSet()) {
        if (this.comparator.compare(dropCandidate.getT(), newElement.getT()) <= 0) {
          log.debug("Cannot evict enough requests to fit request {}. " + "Rejecting", newElement.getT());
          this.requestsRefused++;
          if (this.storeRejectedRequestsSetting
              .equalsIgnoreCase(RequestAllocatorConfig.StoreRejectedRequestsConfig.ALL.name())) {
            this.requestsRejectedDueToInsufficientEviction.add(newElement.getT());
          }
          return false;
        }
        this.tmpRequirement.subtract(dropCandidate.getResourceRequirement());
        toDrop.add(dropCandidate);
        if (!this.resourcePool.exceedsHardBound(
            ResourceRequirement.add(this.tmpRequirement, newElement.getResourceRequirement(), this.reuse), false)) {
          break;
        }
      }

      for (AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T> drop : toDrop) {
        log.debug("Evicting request {}.", drop.getT());
        this.requestsEvicted++;
        if (this.storeRejectedRequestsSetting
            .equalsIgnoreCase(RequestAllocatorConfig.StoreRejectedRequestsConfig.ALL.name())) {
          this.requestsDropped.add(drop.getT());
        }
        this.elements.remove(drop);
        this.currentRequirement.subtract(drop.getResourceRequirement());
      }
    }

    this.elements.add(newElement);
    this.currentRequirement.add(newElement.getResourceRequirement());
    return true;
  }

  /**
   * @return Whether any calls to {@link #add} have returned false, i.e. some element has been rejected due
   * to strict capacity issues.
   */
  public boolean hasRejectedElement() {
    return this.rejectedElement;
  }

  /**
   * @return Whether the list has reached its soft bound.
   */
  public synchronized boolean isFull() {
    return this.resourcePool.exceedsSoftBound(this.currentRequirement, true);
  }

  /**
   * Log statistics about this {@link ConcurrentBoundedPriorityIterable}.
   */
  public synchronized void logStatistics(Optional<Logger> logger) {
    Logger actualLogger = logger.or(log);
    StringBuilder messageBuilder = new StringBuilder("Statistics for ").
        append(ConcurrentBoundedPriorityIterable.class.getSimpleName()).append(": {");
    messageBuilder.append(this.resourcePool).append(", ");
    messageBuilder.append("totalResourcesUsed: ")
        .append(this.resourcePool.stringifyRequirement(this.currentRequirement)).append(", ");
    messageBuilder.append("maxRequirementPerDimension: ")
        .append(this.resourcePool.stringifyRequirement(this.maxResourceRequirement)).append(", ");
    messageBuilder.append("requestsOffered: ").append(this.requestsOffered).append(", ");
    messageBuilder.append("requestsAccepted: ")
        .append(this.requestsOffered - this.requestsEvicted - this.requestsRefused).append(", ");
    messageBuilder.append("requestsRefused: ").append(this.requestsRefused).append(", ");
    messageBuilder.append("requestsEvicted: ").append(this.requestsEvicted);
    messageBuilder.append("}");
    actualLogger.info(messageBuilder.toString());
  }

  @VisibleForTesting
  void reopen() {
    this.closed = false;
  }

  @Override
  public Iterator<AllocatedRequestsIteratorBase.RequestWithResourceRequirement<T>> iterator() {
    this.closed = true;
    return this.elements.iterator();
  }
}
