package org.apache.helix.task;

/*
 * 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.
 */

import java.util.ArrayDeque;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.HashSet;

import org.apache.helix.HelixException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * RuntimeJobDag is a job DAG that provides the job iterator functionality at runtime (when jobs are
 * actually being assigned per job category). This is to support assignment of jobs based on their
 * categories and quotas. RuntimeJobDag uses the list scheduling algorithm using ready-list and
 * inflight-list to return jobs available for scheduling.
 *
 * NOTE: RuntimeJobDag is not thread-safe.
 */
public class RuntimeJobDag extends JobDag {
  private static final Logger LOG = LoggerFactory.getLogger(RuntimeJobDag.class);
  private static final int DEFAULT_NUM_PARALLEL_JOBS = 1;

  // For job iterator functionality
  private ArrayDeque<String> _readyJobList; // Jobs ready to be scheduled
  private Set<String> _inflightJobList; // Jobs that are scheduled but not yet finished
  private boolean _hasDagChanged; // Flag for DAG modification for job queues; if true, ready-list
                                  // must be re-computed
  private Map<String, Set<String>> _successorMap; // Two dependency maps for populating ready-list
  private Map<String, Set<String>> _predecessorMap; // when jobs are finished
  private boolean _isJobQueue;
  private int _numParallelJobs;
  private String _lastJob;

  /**
   * Constructor for Job DAG.
   */
  public RuntimeJobDag() {
    // For job list iterator scheduling
    _readyJobList = new ArrayDeque<>();
    _inflightJobList = new HashSet<>();
    _hasDagChanged = true;
  }

  public RuntimeJobDag(JobDag jobDag, boolean isJobQueue, int numParallelJobs) {
    this._childrenToParents = jobDag.getChildrenToParents();
    this._parentsToChildren = jobDag.getParentsToChildren();
    this._allNodes = jobDag.getAllNodes();
    this._isJobQueue = isJobQueue;
    this._numParallelJobs = numParallelJobs <= 0 ? DEFAULT_NUM_PARALLEL_JOBS : numParallelJobs;
    generateJobList();
  }

  @Override
  public void addParentToChild(String parent, String child) {
    _hasDagChanged = true;
    super.addParentToChild(parent, child);
  }

  @Override
  public void addNode(String node) {
    _hasDagChanged = true;
    super.addNode(node);
  }

  /**
   * Remove a node from the DAG and set the changed flag to true.
   * @param job
   * @param maintainDependency: if true, the removed job's parent and child node will be linked
   *          together,
   *          otherwise, the job will be removed directly without modifying the existing dependency
   *          links.
   */
  public void removeNode(String job, boolean maintainDependency) {
    _hasDagChanged = true;
    super.removeNode(job, maintainDependency);
  }

  /**
   * Returns true if the job iterator has jobs. If the job DAG has been modified, re-generates
   * ready-list.
   * @return true if the iterator has more elements
   */
  public boolean hasNextJob() {
    if (_hasDagChanged) {
      generateJobList(); // Regenerate the ready list
    }
    return !_readyJobList.isEmpty();
  }

  /**
   * Returns next unscheduled job from the job iterator. If the job DAG has been modified,
   * re-generates ready-list.
   * @return job name. Null if the readyJobList is empty.
   */
  @Override
  public String getNextJob() {
    if (_hasDagChanged) {
      generateJobList(); // Regenerate the ready list
    }
    // If list is empty, return null
    if (_readyJobList.isEmpty()) {
      return null;
    }
    String nextJob = _readyJobList.poll();
    _inflightJobList.add(nextJob);
    _lastJob = nextJob;
    return nextJob;
  }

  /**
   * Removes a finished job from the job iterator. If the DAG has been changed, it returns false and leaves an error log. The job must be in the FINISHED state before this call.
   * @param job name of the job to be removed
   */
  public boolean finishJob(String job) {
    if (_hasDagChanged) {
      LOG.warn("Job DAG has been modified; Cannot finish job: {}", job);
      return false;
    }
    if (!_inflightJobList.remove(job)) {
      // this job is not in in-flight list
      LOG.warn(
          String.format("Job: %s has either finished already, never been scheduled, or been removed from DAG", job));
    }
    // Add finished job's successors to ready-list
    if (_isJobQueue) {
      if (_lastJob != null && _parentsToChildren.containsKey(_lastJob)) {
        _readyJobList.offer(_parentsToChildren.get(_lastJob).iterator().next());
      }
    } else if (_successorMap.containsKey(job)) {
        for (String successor : _successorMap.get(job)) {
          // Remove finished job from predecessor map
        if (_predecessorMap.containsKey(successor)) {
          Set<String> predecessors = _predecessorMap.get(successor);
          predecessors.remove(job);
          // Successor must have no predecessors before being added to ready-list
          if (predecessors.isEmpty()) {
            _readyJobList.offer(successor);
          }
        }
      }
    }
    _successorMap.remove(job);
    return true;
  }

  /**
   * Must be called BEFORE using job iterator method and AFTER a full DAG has been added.
   * Resets all job lists and regenerates the ready list. Also, sets the job DAG change flag back to
   * false.
   * The reason this method cannot be in the constructor is that DAG is empty at initialization,
   * and only the client will know when they are done with adding individual jobs to DAG and job
   * list is ready to be created.
   */
  public void generateJobList() {
    resetJobListAndDependencyMaps();
    computeIndependentNodes();
    _readyJobList.addAll(_independentNodes);
    if (_isJobQueue && _readyJobList.size() > 0) {
      // For job queue, only get number of parallel jobs to run in the ready list.
      for (int i = 1; i < _numParallelJobs; i++) {
        if (_parentsToChildren.containsKey(_readyJobList.peekLast())) {
          _readyJobList.offer(_parentsToChildren.get(_readyJobList.peekLast()).iterator().next());
        }
      }
    }
    _hasDagChanged = false;
  }

  /**
   * Re-initialize ready and in-flight lists. Also perform a deep-copy of dependency maps because
   * iterator methods will modify successor and predecessor relationships.
   */
  private void resetJobListAndDependencyMaps() {
    _readyJobList = new ArrayDeque<>();
    _inflightJobList = new HashSet<>();
    _successorMap = new HashMap<>();
    _predecessorMap = new HashMap<>();
    for (Map.Entry<String, Set<String>> entry : _parentsToChildren.entrySet()) {
      _successorMap.put(entry.getKey(), new HashSet<>(entry.getValue()));
    }
    for (Map.Entry<String, Set<String>> entry : _childrenToParents.entrySet()) {
      _predecessorMap.put(entry.getKey(), new HashSet<>(entry.getValue()));
    }
  }

  public Set<String> getInflightJobList() {
    return new HashSet<>(_inflightJobList);
  }

}
