/*
 * 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.lucene.replicator.nrt;

import java.io.Closeable;
import java.io.IOException;
import java.util.PriorityQueue;
import org.apache.lucene.store.AlreadyClosedException;

/**
 * Runs CopyJob(s) in background thread; each ReplicaNode has an instance of this running. At a
 * given there could be one NRT copy job running, and multiple pre-warm merged segments jobs.
 */
class Jobs extends Thread implements Closeable {

  private final PriorityQueue<CopyJob> queue = new PriorityQueue<>();

  private final Node node;

  public Jobs(Node node) {
    this.node = node;
  }

  private boolean finish;

  /**
   * Returns null if we are closing, else, returns the top job or waits for one to arrive if the
   * queue is empty.
   */
  private synchronized SimpleCopyJob getNextJob() {
    while (true) {
      if (finish) {
        return null;
      } else if (queue.isEmpty()) {
        try {
          wait();
        } catch (InterruptedException ie) {
          throw new RuntimeException(ie);
        }
      } else {
        return (SimpleCopyJob) queue.poll();
      }
    }
  }

  @Override
  public void run() {
    while (true) {
      SimpleCopyJob topJob = getNextJob();
      if (topJob == null) {
        assert finish;
        break;
      }

      this.setName("jobs o" + topJob.ord);

      assert topJob != null;

      boolean result;
      try {
        result = topJob.visit();
      } catch (Throwable t) {
        if ((t instanceof AlreadyClosedException) == false) {
          node.message("exception during job.visit job=" + topJob + "; now cancel");
          t.printStackTrace(System.out);
        } else {
          node.message("AlreadyClosedException during job.visit job=" + topJob + "; now cancel");
        }
        try {
          topJob.cancel("unexpected exception in visit", t);
        } catch (Throwable t2) {
          node.message("ignore exception calling cancel: " + t2);
          t2.printStackTrace(System.out);
        }
        try {
          topJob.onceDone.run(topJob);
        } catch (Throwable t2) {
          node.message("ignore exception calling OnceDone: " + t2);
          t2.printStackTrace(System.out);
        }
        continue;
      }

      if (result == false) {
        // Job isn't done yet; put it back:
        synchronized (this) {
          queue.offer(topJob);
        }
      } else {
        // Job finished, now notify caller:
        try {
          topJob.onceDone.run(topJob);
        } catch (Throwable t) {
          node.message("ignore exception calling OnceDone: " + t);
          t.printStackTrace(System.out);
        }
      }
    }

    node.message("top: jobs now exit run thread");

    synchronized (this) {
      // Gracefully cancel any jobs we didn't finish:
      while (queue.isEmpty() == false) {
        SimpleCopyJob job = (SimpleCopyJob) queue.poll();
        node.message("top: Jobs: now cancel job=" + job);
        try {
          job.cancel("jobs closing", null);
        } catch (Throwable t) {
          node.message("ignore exception calling cancel");
          t.printStackTrace(System.out);
        }
        try {
          job.onceDone.run(job);
        } catch (Throwable t) {
          node.message("ignore exception calling OnceDone");
          t.printStackTrace(System.out);
        }
      }
    }
  }

  public synchronized void launch(CopyJob job) {
    if (finish == false) {
      queue.offer(job);
      notify();
    } else {
      throw new AlreadyClosedException("closed");
    }
  }

  /** Cancels any existing jobs that are copying the same file names as this one */
  public synchronized void cancelConflictingJobs(CopyJob newJob) throws IOException {
    for (CopyJob job : queue) {
      if (job.conflicts(newJob)) {
        node.message(
            "top: now cancel existing conflicting job=" + job + " due to newJob=" + newJob);
        job.cancel("conflicts with new job", null);
      }
    }
  }

  @Override
  public synchronized void close() {
    finish = true;
    notify();
    try {
      join();
    } catch (InterruptedException ie) {
      throw new RuntimeException(ie);
    }
  }
}
