/*
 * 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.ambari.server.topology;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Calendar;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Callable service implementation for executing tasks asynchronously.
 * The service repeatedly tries to execute the provided task till it successfully completes, or the provided timeout
 * interval is exceeded.
 *
 * @param <T> the type returned by the task to be executed
 */
public class AsyncCallableService<T> implements Callable<Boolean> {

  private static final Logger LOG = LoggerFactory.getLogger(AsyncCallableService.class);

  // task execution is done on a separate thread provided by this executor
  private final ScheduledExecutorService executorService;

  // the task to be executed
  private final Callable<T> task;

  // the total time the allowed for the task to be executed (retries will be happen within this timeframe in
  // milliseconds)
  private final long timeout;

  // the delay between two consecutive execution trials in milliseconds
  private final long delay;


  private Boolean serviceResult = Boolean.FALSE;

  public AsyncCallableService(Callable<T> task, long timeout, long delay,
                              ScheduledExecutorService executorService) {
    this.task = task;
    this.executorService = executorService;
    this.timeout = timeout;
    this.delay = delay;
  }

  @Override
  public Boolean call() {

    long startTimeInMillis = Calendar.getInstance().getTimeInMillis();
    LOG.info("Task execution started at: {}", startTimeInMillis);

    // task execution started on a new thread
    Future future = executorService.submit(task);

    while (!taskCompleted(future)) {
      if (!timeoutExceeded(startTimeInMillis)) {
        LOG.debug("Retrying task execution in [ {} ] milliseconds.", delay);
        future = executorService.schedule(task, delay, TimeUnit.MILLISECONDS);
      } else {
        LOG.debug("Timout exceeded, cancelling task ... ");
        // making sure the task gets cancelled!
        if (!future.isDone()) {
          boolean cancelled = future.cancel(true);
          LOG.debug("Task cancelled: {}", cancelled);
        } else {
          LOG.debug("Task already done.");
        }
        LOG.info("Timeout exceeded, task execution won't be retried!");
        // exit the "retry" loop!
        break;
      }
    }

    LOG.info("Exiting Async task execution with the result: [ {} ]", serviceResult);
    return serviceResult;
  }

  private boolean taskCompleted(Future<T> future) {
    boolean completed = false;
    try {
      LOG.debug("Retrieving task execution result ...");
      // should receive task execution result within the configured timeout interval
      // exceptions thrown from the task are propagated here
      T taskResult = future.get(timeout, TimeUnit.MILLISECONDS);

      // task failures are expected to be reportesd as exceptions
      LOG.debug("Task successfully executed: {}", taskResult);
      setServiceResult(Boolean.TRUE);
      completed = true;
    } catch (Exception e) {
      // Future.isDone always true here!
      LOG.info("Exception during task execution: ", e);
    }
    return completed;
  }

  private boolean timeoutExceeded(long startTimeInMillis) {
    return timeout < Calendar.getInstance().getTimeInMillis() - startTimeInMillis;
  }

  private void setServiceResult(Boolean serviceResult) {
    this.serviceResult = serviceResult;
  }

}
