/**
* 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.hadoop.yarn.server.resourcemanager;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience.Private;
import org.apache.hadoop.net.NetworkTopology;
import org.apache.hadoop.yarn.api.protocolrecords.GetNewApplicationRequest;
import org.apache.hadoop.yarn.api.protocolrecords.GetNewApplicationResponse;
import org.apache.hadoop.yarn.api.protocolrecords.StartContainerRequest;
import org.apache.hadoop.yarn.api.protocolrecords.StartContainersRequest;
import org.apache.hadoop.yarn.api.protocolrecords.StopContainersRequest;
import org.apache.hadoop.yarn.api.protocolrecords.SubmitApplicationRequest;
import org.apache.hadoop.yarn.api.records.ApplicationAttemptId;
import org.apache.hadoop.yarn.api.records.ApplicationId;
import org.apache.hadoop.yarn.api.records.ApplicationSubmissionContext;
import org.apache.hadoop.yarn.api.records.Container;
import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.api.records.ContainerLaunchContext;
import org.apache.hadoop.yarn.api.records.Priority;
import org.apache.hadoop.yarn.api.records.Resource;
import org.apache.hadoop.yarn.api.records.ResourceRequest;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.exceptions.YarnException;
import org.apache.hadoop.yarn.factories.RecordFactory;
import org.apache.hadoop.yarn.factory.providers.RecordFactoryProvider;
import org.apache.hadoop.yarn.server.resourcemanager.Task.State;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.Allocation;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.NodeType;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.ResourceScheduler;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.event.AppAddedSchedulerEvent;
import org.apache.hadoop.yarn.server.resourcemanager.scheduler.event.AppAttemptAddedSchedulerEvent;
import org.apache.hadoop.yarn.util.Records;
import org.apache.hadoop.yarn.util.resource.Resources;

@Private
public class Application {
  private static final Log LOG = LogFactory.getLog(Application.class);
  
  private AtomicInteger taskCounter = new AtomicInteger(0);

  private AtomicInteger numAttempts = new AtomicInteger(0);
  final private String user;
  final private String queue;
  final private ApplicationId applicationId;
  final private ApplicationAttemptId applicationAttemptId;
  final private ResourceManager resourceManager;
  private final static RecordFactory recordFactory = RecordFactoryProvider.getRecordFactory(null);
  
  final private Map<Priority, Resource> requestSpec = 
    new TreeMap<Priority, Resource>(
        new org.apache.hadoop.yarn.server.resourcemanager.resource.Priority.Comparator());
  
  final private Map<Priority, Map<String, ResourceRequest>> requests = 
    new TreeMap<Priority, Map<String, ResourceRequest>>(
        new org.apache.hadoop.yarn.server.resourcemanager.resource.Priority.Comparator());
  
  final Map<Priority, Set<Task>> tasks = 
    new TreeMap<Priority, Set<Task>>(
        new org.apache.hadoop.yarn.server.resourcemanager.resource.Priority.Comparator());
  
  final private Set<ResourceRequest> ask = 
    new TreeSet<ResourceRequest>(
        new org.apache.hadoop.yarn.api.records.ResourceRequest.ResourceRequestComparator());

  final private Map<String, NodeManager> nodes = 
    new HashMap<String, NodeManager>();
  
  Resource used = recordFactory.newRecordInstance(Resource.class);
  
  public Application(String user, ResourceManager resourceManager) 
      throws YarnException {
    this(user, "default", resourceManager);
  }
  
  public Application(String user, String queue, ResourceManager resourceManager) 
      throws YarnException {
    this.user = user;
    this.queue = queue;
    this.resourceManager = resourceManager;
    // register an application
    GetNewApplicationRequest request =
            Records.newRecord(GetNewApplicationRequest.class);
    GetNewApplicationResponse newApp = 
        this.resourceManager.getClientRMService().getNewApplication(request);
    this.applicationId = newApp.getApplicationId();
  
    this.applicationAttemptId =
        ApplicationAttemptId.newInstance(this.applicationId,
          this.numAttempts.getAndIncrement());
  }

  public String getUser() {
    return user;
  }

  public String getQueue() {
    return queue;
  }

  public ApplicationId getApplicationId() {
    return applicationId;
  }
  
  public ApplicationAttemptId getApplicationAttemptId() {
    return applicationAttemptId;
  }

  public static String resolve(String hostName) {
    return NetworkTopology.DEFAULT_RACK;
  }
  
  public int getNextTaskId() {
    return taskCounter.incrementAndGet();
  }
  
  public Resource getUsedResources() {
    return used;
  }
  
  @SuppressWarnings("deprecation")
  public synchronized void submit() throws IOException, YarnException {
    ApplicationSubmissionContext context = recordFactory.newRecordInstance(ApplicationSubmissionContext.class);
    context.setApplicationId(this.applicationId);
    context.setQueue(this.queue);
    
    // Set up the container launch context for the application master
    ContainerLaunchContext amContainer
        = Records.newRecord(ContainerLaunchContext.class);
    context.setAMContainerSpec(amContainer);
    context.setResource(Resources.createResource(
        YarnConfiguration.DEFAULT_RM_SCHEDULER_MINIMUM_ALLOCATION_MB));
    
    SubmitApplicationRequest request = recordFactory
        .newRecordInstance(SubmitApplicationRequest.class);
    request.setApplicationSubmissionContext(context);
    final ResourceScheduler scheduler = resourceManager.getResourceScheduler();
    
    resourceManager.getClientRMService().submitApplication(request);

    // Notify scheduler
    AppAddedSchedulerEvent addAppEvent =
        new AppAddedSchedulerEvent(this.applicationId, this.queue, "user");
    scheduler.handle(addAppEvent);
    AppAttemptAddedSchedulerEvent addAttemptEvent =
        new AppAttemptAddedSchedulerEvent(this.applicationAttemptId, false);
    scheduler.handle(addAttemptEvent);
  }
  
  public synchronized void addResourceRequestSpec(
      Priority priority, Resource capability) {
    Resource currentSpec = requestSpec.put(priority, capability);
    if (currentSpec != null) {
      throw new IllegalStateException("Resource spec already exists for " +
      		"priority " + priority.getPriority() + " - " + currentSpec.getMemory());
    }
  }
  
  public synchronized void addNodeManager(String host,
      int containerManagerPort, NodeManager nodeManager) {
    nodes.put(host + ":" + containerManagerPort, nodeManager);
  }
  
  private synchronized NodeManager getNodeManager(String host) {
    return nodes.get(host);
  }
  
  public synchronized void addTask(Task task) {
    Priority priority = task.getPriority();
    Map<String, ResourceRequest> requests = this.requests.get(priority);
    if (requests == null) {
      requests = new HashMap<String, ResourceRequest>();
      this.requests.put(priority, requests);
      if(LOG.isDebugEnabled()) {
        LOG.debug("Added priority=" + priority + " application="
          + applicationId);
      }
    }
    
    final Resource capability = requestSpec.get(priority);
    
    // Note down the task
    Set<Task> tasks = this.tasks.get(priority);
    if (tasks == null) {
      tasks = new HashSet<Task>();
      this.tasks.put(priority, tasks);
    }
    tasks.add(task);
    
    LOG.info("Added task " + task.getTaskId() + " to application " + 
        applicationId + " at priority " + priority);
    
    if(LOG.isDebugEnabled()) {
      LOG.debug("addTask: application=" + applicationId
        + " #asks=" + ask.size());
    }
    
    // Create resource requests
    for (String host : task.getHosts()) {
      // Data-local
      addResourceRequest(priority, requests, host, capability);
    }
        
    // Rack-local
    for (String rack : task.getRacks()) {
      addResourceRequest(priority, requests, rack, capability);
    }
      
    // Off-switch
    addResourceRequest(priority, requests, ResourceRequest.ANY, capability);
  }
  
  public synchronized void finishTask(Task task) throws IOException,
      YarnException {
    Set<Task> tasks = this.tasks.get(task.getPriority());
    if (!tasks.remove(task)) {
      throw new IllegalStateException(
          "Finishing unknown task " + task.getTaskId() + 
          " from application " + applicationId);
    }
    
    NodeManager nodeManager = task.getNodeManager();
    ContainerId containerId = task.getContainerId();
    task.stop();
    List<ContainerId> containerIds = new ArrayList<ContainerId>();
    containerIds.add(containerId);
    StopContainersRequest stopRequest =
        StopContainersRequest.newInstance(containerIds);
    nodeManager.stopContainers(stopRequest);
    
    Resources.subtractFrom(used, requestSpec.get(task.getPriority()));
    
    LOG.info("Finished task " + task.getTaskId() + 
        " of application " + applicationId + 
        " on node " + nodeManager.getHostName() + 
        ", currently using " + used + " resources");
  }
  
  private synchronized void addResourceRequest(
      Priority priority, Map<String, ResourceRequest> requests, 
      String resourceName, Resource capability) {
    ResourceRequest request = requests.get(resourceName);
    if (request == null) {
      request = 
        org.apache.hadoop.yarn.server.utils.BuilderUtils.newResourceRequest(
            priority, resourceName, capability, 1);
      requests.put(resourceName, request);
    } else {
      request.setNumContainers(request.getNumContainers() + 1);
    }
    
    // Note this down for next interaction with ResourceManager
    ask.remove(request);
    ask.add(
        org.apache.hadoop.yarn.server.utils.BuilderUtils.newResourceRequest(
            request)); // clone to ensure the RM doesn't manipulate the same obj
    
    if(LOG.isDebugEnabled()) {
      LOG.debug("addResourceRequest: applicationId=" + applicationId.getId()
        + " priority=" + priority.getPriority()
        + " resourceName=" + resourceName + " capability=" + capability
        + " numContainers=" + request.getNumContainers()
        + " #asks=" + ask.size());
    }
  }
  
  public synchronized List<Container> getResources() throws IOException {
    if(LOG.isDebugEnabled()) {
      LOG.debug("getResources begin:" + " application=" + applicationId
        + " #ask=" + ask.size());

      for (ResourceRequest request : ask) {
        LOG.debug("getResources:" + " application=" + applicationId
          + " ask-request=" + request);
      }
    }
    
    // Get resources from the ResourceManager
    Allocation allocation = resourceManager.getResourceScheduler().allocate(
        applicationAttemptId, new ArrayList<ResourceRequest>(ask),
        new ArrayList<ContainerId>(), null, null);
    System.out.println("-=======" + applicationAttemptId);
    System.out.println("----------" + resourceManager.getRMContext().getRMApps()
        .get(applicationId).getRMAppAttempt(applicationAttemptId));
    List<Container> containers = allocation.getContainers();

    // Clear state for next interaction with ResourceManager
    ask.clear();
    
    if(LOG.isDebugEnabled()) {
      LOG.debug("getResources() for " + applicationId + ":"
        + " ask=" + ask.size() + " recieved=" + containers.size());
    }
    
    return containers;
  }
  
  public synchronized void assign(List<Container> containers) 
  throws IOException, YarnException {
    
    int numContainers = containers.size();
    // Schedule in priority order
    for (Priority priority : requests.keySet()) {
      assign(priority, NodeType.NODE_LOCAL, containers);
      assign(priority, NodeType.RACK_LOCAL, containers);
      assign(priority, NodeType.OFF_SWITCH, containers);

      if (containers.isEmpty()) { 
        break;
      }
    }
    
    int assignedContainers = numContainers - containers.size();
    LOG.info("Application " + applicationId + " assigned " + 
        assignedContainers + "/" + numContainers);
  }
  
  public synchronized void schedule() throws IOException, YarnException {
    assign(getResources());
  }
  
  private synchronized void assign(Priority priority, NodeType type, 
      List<Container> containers) throws IOException, YarnException {
    for (Iterator<Container> i=containers.iterator(); i.hasNext();) {
      Container container = i.next();
      String host = container.getNodeId().toString();
      
      if (Resources.equals(requestSpec.get(priority), container.getResource())) { 
        // See which task can use this container
        for (Iterator<Task> t=tasks.get(priority).iterator(); t.hasNext();) {
          Task task = t.next();
          if (task.getState() == State.PENDING && task.canSchedule(type, host)) {
            NodeManager nodeManager = getNodeManager(host);
            
            task.start(nodeManager, container.getId());
            i.remove();
            
            // Track application resource usage
            Resources.addTo(used, container.getResource());
            
            LOG.info("Assigned container (" + container + ") of type " + type +
                " to task " + task.getTaskId() + " at priority " + priority + 
                " on node " + nodeManager.getHostName() +
                ", currently using " + used + " resources");

            // Update resource requests
            updateResourceRequests(requests.get(priority), type, task);

            // Launch the container
            StartContainerRequest scRequest =
                StartContainerRequest.newInstance(createCLC(),
                  container.getContainerToken());
            List<StartContainerRequest> list =
                new ArrayList<StartContainerRequest>();
            list.add(scRequest);
            StartContainersRequest allRequests =
                StartContainersRequest.newInstance(list);
            nodeManager.startContainers(allRequests);
            break;
          }
        }
      }
    }
  }

  private void updateResourceRequests(Map<String, ResourceRequest> requests, 
      NodeType type, Task task) {
    if (type == NodeType.NODE_LOCAL) {
      for (String host : task.getHosts()) {
        if(LOG.isDebugEnabled()) {
          LOG.debug("updateResourceRequests:" + " application=" + applicationId
            + " type=" + type + " host=" + host
            + " request=" + ((requests == null) ? "null" : requests.get(host)));
        }
        updateResourceRequest(requests.get(host));
      }
    }
    
    if (type == NodeType.NODE_LOCAL || type == NodeType.RACK_LOCAL) {
      for (String rack : task.getRacks()) {
        if(LOG.isDebugEnabled()) {
          LOG.debug("updateResourceRequests:" + " application=" + applicationId
            + " type=" + type + " rack=" + rack
            + " request=" + ((requests == null) ? "null" : requests.get(rack)));
        }
        updateResourceRequest(requests.get(rack));
      }
    }
    
    updateResourceRequest(requests.get(ResourceRequest.ANY));
    
    if(LOG.isDebugEnabled()) {
      LOG.debug("updateResourceRequests:" + " application=" + applicationId
        + " #asks=" + ask.size());
    }
  }
  
  private void updateResourceRequest(ResourceRequest request) {
    request.setNumContainers(request.getNumContainers() - 1);

    // Note this for next interaction with ResourceManager
    ask.remove(request);
    ask.add(
        org.apache.hadoop.yarn.server.utils.BuilderUtils.newResourceRequest(
        request)); // clone to ensure the RM doesn't manipulate the same obj

    if(LOG.isDebugEnabled()) {
      LOG.debug("updateResourceRequest:" + " application=" + applicationId
        + " request=" + request);
    }
  }

  private ContainerLaunchContext createCLC() {
    ContainerLaunchContext clc = recordFactory.newRecordInstance(ContainerLaunchContext.class);
    return clc;
  }
}