/**
 * 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 storm.mesos.resources;

import org.apache.storm.scheduler.TopologyDetails;
import org.apache.mesos.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import storm.mesos.util.MesosCommon;
import storm.mesos.util.PrettyProtobuf;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AggregatedOffers {

  private final Logger log = LoggerFactory.getLogger(AggregatedOffers.class);
  private Map<ResourceType, Resource> availableResources;

  private List<Protos.Offer> offerList = new ArrayList<Protos.Offer>();

  private final String hostname;

  private Protos.SlaveID slaveID;

  private void initializeAvailableResources() {
    availableResources = new HashMap<>();
    availableResources.put(ResourceType.CPU, new ScalarResource(ResourceType.CPU));
    availableResources.put(ResourceType.MEM, new ScalarResource(ResourceType.MEM));
    availableResources.put(ResourceType.PORTS, new RangeResource(ResourceType.PORTS));
  }

  public AggregatedOffers(Protos.Offer offer) {
    initializeAvailableResources();
    this.slaveID = offer.getSlaveId();
    this.hostname = offer.getHostname();
    add(offer);
  }

  public String getHostname() {
    return hostname;
  }

  public void add(Protos.Offer offer) {
    // We are unable to aggregate offers if they are from different mesos slaves/workers/agents
    assert offer.getSlaveId().equals(slaveID) && offer.getHostname().equals(hostname);
    offerList.add(offer);

    for (Protos.Resource r : offer.getResourcesList()) {
      ResourceType resourceType = ResourceType.of(r.getName());
      ReservationType reservationType = (r.getRole().equals("*")) ?
                                        ReservationType.UNRESERVED : ReservationType.STATIC;

      if (r.hasReservation()) {
        // skip resources with dynamic reservations
        continue;
      }

      switch (resourceType) {
        case CPU:
        case MEM:
          ResourceEntries.ScalarResourceEntry scalarResourceEntry = new ResourceEntries.ScalarResourceEntry(reservationType, r.getScalar().getValue());
          availableResources.get(resourceType).add(scalarResourceEntry, reservationType);
          break;
        case PORTS:
          for (Protos.Value.Range range : r.getRanges().getRangeList()) {
            ResourceEntries.RangeResourceEntry rangeResourceEntry = new ResourceEntries.RangeResourceEntry(reservationType, range.getBegin(), range.getEnd());
            availableResources.get(resourceType).add(rangeResourceEntry, reservationType);
          }
          break;
        case DISK:
          // TODO: Support disk resource isolation (https://github.com/mesos/storm/issues/147)
          break;
        default:
          log.warn(String.format("Found unsupported resourceType '%s' while adding offer %s", resourceType, PrettyProtobuf.offerToString(offer)));
      }
    }
  }

  public boolean isAvailable(ResourceType resourceType, ResourceEntry<?> resource) {
    return availableResources.get(resourceType).isAvailable(resource);
  }

  /**
   * Unused Method - Exists for the purpose of facilitating support of reservations.
   * TODO: Support reservations (https://github.com/mesos/storm/issues/148)
   * For more information about why this unused code exists, see discussion: https://github.com/mesos/storm/pull/146#issuecomment-225496075
   */
  public boolean isAvailable(ResourceType resourceType, ReservationType reservationType, ResourceEntry<?> resource) {
    return availableResources.get(resourceType).isAvailable(resource, reservationType);
  }

  public <T extends ResourceEntry> List<T> getAllAvailableResources(ResourceType resourceType) {
    return availableResources.get(resourceType).getAllAvailableResources();
  }

  /**
   * Unused Method - Exists for the purpose of facilitating support of reservations.
   * TODO: Support reservations (https://github.com/mesos/storm/issues/148)
   * For more information about why this unused code exists, see discussion: https://github.com/mesos/storm/pull/146#issuecomment-225496075
   */
  public <T extends ResourceEntry> List<T> getAllAvailableResources(ResourceType resourceType, ReservationType reservationType) {
    return availableResources.get(resourceType).getAllAvailableResources(reservationType);
  }

  public void reserve(ResourceType resourceType, ResourceEntry<?> resource) throws ResourceNotAvailableException {
    if (availableResources.get(resourceType).isAvailable(resource)) {
      availableResources.get(resourceType).removeAndGet(resource);
    }
  }

  public List<ResourceEntry> reserveAndGet(ResourceType resourceType, ResourceEntry<?> resource) throws ResourceNotAvailableException {
    if (availableResources.get(resourceType).isAvailable(resource)) {
      return availableResources.get(resourceType).removeAndGet(resource);
    }
    return new ArrayList<>();
  }

  /**
   * Unused Method - Exists for the purpose of facilitating support of reservations.
   * TODO: Support reservations (https://github.com/mesos/storm/issues/148)
   * For more information about why this unused code exists, see discussion: https://github.com/mesos/storm/pull/146#issuecomment-225496075
   */
  public List<ResourceEntry> reserveAndGet(ResourceType resourceType, ReservationType reservationType, ResourceEntry<?> resource) throws
    ResourceNotAvailableException {
    if (availableResources.get(resourceType).isAvailable(resource, reservationType)) {
      return availableResources.get(resourceType).removeAndGet(resource, reservationType);
    }
    return new ArrayList<>();
  }

  public List<Protos.OfferID> getOfferIDList() {
    List<Protos.OfferID> offerIDList = new ArrayList<>();
    for (Protos.Offer offer: offerList) {
      offerIDList.add(offer.getId());
    }
    return offerIDList;
  }

  public Protos.SlaveID getSlaveID() {
    return slaveID;
  }

  @Override
  public String toString() {
    return String.format("%s, %s, %s",
                         availableResources.get(ResourceType.CPU),
                         availableResources.get(ResourceType.MEM),
                         availableResources.get(ResourceType.PORTS));
  }


  public boolean isFit(Map mesosStormConf, TopologyDetails topologyDetails, boolean supervisorExists) {
    double requestedWorkerCpu = MesosCommon.topologyWorkerCpu(mesosStormConf, topologyDetails);
    double requestedWorkerMem = MesosCommon.topologyWorkerMem(mesosStormConf, topologyDetails);

    requestedWorkerCpu += supervisorExists ? 0 : MesosCommon.executorCpu(mesosStormConf);
    requestedWorkerMem += supervisorExists ? 0 : MesosCommon.executorMem(mesosStormConf);

    return (isAvailable(ResourceType.CPU, new ResourceEntries.ScalarResourceEntry(requestedWorkerCpu)) &&
            isAvailable(ResourceType.MEM, new ResourceEntries.ScalarResourceEntry(requestedWorkerMem)) &&
            !getAllAvailableResources(ResourceType.PORTS).isEmpty());
  }

  public boolean isFit(Map mesosStormConf, TopologyDetails topologyDetails, Long port, boolean supervisorExists) {
    double requestedWorkerCpu = MesosCommon.topologyWorkerCpu(mesosStormConf, topologyDetails);
    double requestedWorkerMem = MesosCommon.topologyWorkerMem(mesosStormConf, topologyDetails);

    requestedWorkerCpu += supervisorExists ? 0 : MesosCommon.executorCpu(mesosStormConf);
    requestedWorkerMem += supervisorExists ? 0 : MesosCommon.executorMem(mesosStormConf);

    return (isAvailable(ResourceType.CPU, new ResourceEntries.ScalarResourceEntry(requestedWorkerCpu)) &&
            isAvailable(ResourceType.MEM, new ResourceEntries.ScalarResourceEntry(requestedWorkerMem)) &&
            isAvailable(ResourceType.PORTS, new ResourceEntries.RangeResourceEntry(port, port)));
  }
}