/**
 * Copyright 2008 Google Inc.
 *
 * Licensed 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.waveprotocol.wave.client.scheduler;

import org.waveprotocol.wave.client.scheduler.Scheduler.IncrementalTask;
import org.waveprotocol.wave.client.scheduler.Scheduler.Priority;
import org.waveprotocol.wave.client.scheduler.Scheduler.Schedulable;
import org.waveprotocol.wave.client.scheduler.Scheduler.Task;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.IntMap;

import java.util.LinkedList;
import java.util.Queue;

/**
 * Optimised data structure used by BrowserBackedScheduler to store currently running jobs.
 *
 * @author danilatos@google.com (Daniel Danilatos)
 */
public class JobRegistry {

  /**
   * Jobs at each priority.
   *
   * Array of priority ordinals to ordered maps (which JSO maps inherently are)
   * Each map is an ordered map of id to job.
   */
  private final IntMap<Queue<Schedulable>> priorities = CollectionUtils.createIntMap();

  /** Controller that collects job counts. */
  private final Controller jobCounter;

  /** Used as part of a pair in a return value, to avoid creating an object */
  private Schedulable returnJob;

  /** Number of jobs in the registry, to provide fast {@link #isEmpty()}. */
  private int jobCount;

  /**
   * Creates a job registry.
   *
   * @param jobCounter  counter of jobs
   */
  public JobRegistry(Controller jobCounter) {
    this.jobCounter = jobCounter;

    for (Priority p : Priority.values()) {
      // TODO(zdwang): Use a fast LinkedList that doens't mak this class untestable.
      priorities.put(p.ordinal(), new LinkedList<Schedulable>());
    }
  }

  /**
   * Add a job at the given priority
   *
   * @param priority
   * @param job
   */
  public void add(Priority priority, Schedulable job) {
    assert job != null : "tried to add null job";
    Queue<Schedulable> queue = priorities.get(priority.ordinal());
    if (queue.remove(job)) {
      // Do nothing else
      queue.add(job);
      return;
    } else {
      queue.add(job);
      jobCount++;
      jobCounter.jobAdded(priority, job);
    }
  }

  /**
   * @param priority
   * @return the number of jobs at the given priority.
   */
  public int numJobsAtPriority(Priority priority) {
    return priorities.get(priority.ordinal()).size();
  }

  /**
   * Remove the first job for the given priority.
   * The job and its id can be retrieved by the various getRemoved* methods
   * (This is to avoid creating a pair object as a return value).
   *
   * @param priority
   */
  public void removeFirst(Priority priority) {
    Queue<Schedulable> queue = priorities.get(priority.ordinal());
    if (queue.isEmpty()) {
      returnJob = null;
    } else {
      returnJob = queue.poll();
      jobCount--;
      jobCounter.jobRemoved(priority, returnJob);
    }
  }

  /**
   * Obliterate the job with the given priority and id.
   * Does NOT store information to be retrieved like {@link #removeFirst(Priority)} does.
   *
   * @param priority
   * @param id
   */
  public void remove(Priority priority, Schedulable job) {
    if (priorities.get(priority.ordinal()).remove(job)) {
      jobCount--;
      jobCounter.jobRemoved(priority, job);
    }
  }

  /**
   * @return Job removed by {@link #removeFirst(Priority)}
   */
  public Schedulable getRemovedJob() {
    return returnJob;
  }

  /**
   * @return Job removed by {@link #removeFirst(Priority)}, Cast to IncrementalTask.
   */
  public IncrementalTask getRemovedJobAsProcess() {
    return (IncrementalTask) returnJob;
  }

  /**
   * @return Job removed by {@link #removeFirst(Priority)}, Cast to Task.
   */
  public Task getRemovedJobAsTask() {
    return (Task) returnJob;
  }

  /**
   * Used for testing
   */
  boolean debugIsClear() {
    for (Priority p : Priority.values()) {
      if (!priorities.get(p.ordinal()).isEmpty()) {
        assert jobCount > 0 : "Count 0 when: " + toString();

        return false;
      }
    }

    assert jobCount == 0 : "Count non-zero when: " + toString();
    return true;
  }

  /**
   * Tests if this registry has any jobs in it.
   *
   * @return true if this registry has no jobs, false otherwise.
   */
  boolean isEmpty() {
    return jobCount == 0;
  }

  /** {@inheritDoc} */
  @Override
  public String toString() {
    // NOTE(user): this causes "too much recursion" errors in Firefox?
    // return priorities.toSource();
    // ... so we do explicit building instead :(
    StringBuilder result = new StringBuilder();
    for (Priority p : Priority.values()) {
      result.append(" { priority: " + p + "; ");
      result.append(" jobs: " + priorities.get(p.ordinal()) + "; } ");
    }
    return result.toString();
  }

  /**
   * @return short description
   */
  public String debugShortDescription() {
    // NOTE(user): this causes "too much recursion" errors in Firefox?
    // return priorities.toSource();
    // ... so we do explicit building instead :(
    StringBuilder result = new StringBuilder();
    for (Priority p : Priority.values()) {
      result.append(" { priority: " + p + "; ");
      result.append(" jobs count: " + priorities.get(p.ordinal()).size() + "; } ");
    }
    return result.toString();
  }
}
