// Copyright (C) 2021 The Android Open Source Project
//
// 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 com.google.gerrit.server.cancellation;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import java.util.HashSet;
import java.util.Set;

/**
 * Context that allows to register {@link RequestStateProvider}s.
 *
 * <p>The registered {@link RequestStateProvider}s are stored in {@link ThreadLocal} so that they
 * can be accessed during the request execution (via {@link #getRequestStateProviders()}.
 *
 * <p>On {@link #close()} the {@link RequestStateProvider}s that have been registered by this {@code
 * RequestStateContext} instance are removed from {@link ThreadLocal}.
 *
 * <p>Nesting {@code RequestStateContext}s is possible.
 *
 * <p>Currently there is no logic to automatically copy the {@link RequestStateContext} to
 * background threads, but implementing this may be considered in the future. This means that by
 * default we only support cancellation of the main thread, but not of background threads. That's
 * fine as all significant work is being done in the main thread.
 *
 * <p>{@link com.google.gerrit.server.util.RequestContext} is also a context that is available for
 * the time of the request, but it is not suitable to manage registrations of {@link
 * RequestStateProvider}s. Hence {@link RequestStateProvider} registrations are managed by a
 * separate context, which is this class, {@link RequestStateContext}:
 *
 * <ul>
 *   <li>{@link com.google.gerrit.server.util.RequestContext} is an interface that has many
 *       implementations and hence cannot manage a {@link ThreadLocal} state.
 *   <li>{@link com.google.gerrit.server.util.RequestContext} is not an {@link AutoCloseable} and
 *       hence cannot cleanup any {@link ThreadLocal} state on close (turning it into an {@link
 *       AutoCloseable} would require a large refactoring).
 *   <li>Despite the name {@link com.google.gerrit.server.util.RequestContext} is not only used for
 *       requests scopes but also for other scopes that are not a request (e.g. plugin invocations,
 *       email sending, manual scopes).
 *   <li>{@link com.google.gerrit.server.util.RequestContext} is not copied to background and should
 *       not be, but for {@link RequestStateContext} we may consider doing this in the future.
 * </ul>
 */
public class RequestStateContext implements AutoCloseable {
  /** The {@link RequestStateProvider}s that have been registered for the thread. */
  private static final ThreadLocal<Set<RequestStateProvider>> threadLocalRequestStateProviders =
      new ThreadLocal<>();

  /** Whether currently a non-cancellable operation is being performed. */
  private static final ThreadLocal<Boolean> inNonCancellableOperation = new ThreadLocal<>();

  /**
   * Aborts the current request by throwing a {@link RequestCancelledException} if any of the
   * registered {@link RequestStateProvider}s reports the request as cancelled.
   *
   * <p>If an atomic operation is currently being performed, request cancellations are ignored and
   * the request doesn't get aborted.
   *
   * @throws RequestCancelledException thrown if the current request is cancelled and should be
   *     aborted
   * @see #startNonCancellableOperation()
   */
  public static void abortIfCancelled() throws RequestCancelledException {
    if (inNonCancellableOperation.get() != null && inNonCancellableOperation.get()) {
      // Do not cancel the request while an atomic operation is being performed.
      return;
    }

    getRequestStateProviders()
        .forEach(
            requestStateProvider ->
                requestStateProvider.checkIfCancelled(
                    (reason, message) -> {
                      throw new RequestCancelledException(reason, message);
                    }));
  }

  /**
   * Starts a non-cancellable operation.
   *
   * <p>If the request was cancelled while the non-cancellable operation was running, it gets
   * aborted on close of the returned {@link AutoCloseable}.
   *
   * @return {@link AutoCloseable} that finishes the non-cancellable operation on close.
   */
  public static NonCancellableOperationContext startNonCancellableOperation() {
    if (inNonCancellableOperation.get() != null && inNonCancellableOperation.get()) {
      // atomic operation is already in progress
      return () -> {};
    }

    inNonCancellableOperation.set(true);
    return () -> {
      inNonCancellableOperation.remove();
      abortIfCancelled();
    };
  }

  /** Returns the {@link RequestStateProvider}s that have been registered for the thread. */
  @VisibleForTesting
  static ImmutableSet<RequestStateProvider> getRequestStateProviders() {
    if (threadLocalRequestStateProviders.get() == null) {
      return ImmutableSet.of();
    }
    return ImmutableSet.copyOf(threadLocalRequestStateProviders.get());
  }

  /** Opens a {@code RequestStateContext}. */
  public static RequestStateContext open() {
    return new RequestStateContext();
  }

  /**
   * The {@link RequestStateProvider}s that have been registered by this {@code
   * RequestStateContext}.
   */
  private Set<RequestStateProvider> requestStateProviders = new HashSet<>();

  private RequestStateContext() {}

  /**
   * Registers a {@link RequestStateProvider}.
   *
   * @param requestStateProvider the {@link RequestStateProvider} that should be registered
   * @return the {@code RequestStateContext} instance for chaining calls
   */
  public RequestStateContext addRequestStateProvider(RequestStateProvider requestStateProvider) {
    if (threadLocalRequestStateProviders.get() == null) {
      threadLocalRequestStateProviders.set(new HashSet<>());
    }
    if (threadLocalRequestStateProviders.get().add(requestStateProvider)) {
      requestStateProviders.add(requestStateProvider);
    }
    return this;
  }

  /**
   * Closes this {@code RequestStateContext}.
   *
   * <p>Ensures that all {@link RequestStateProvider}s that have been registered by this {@code
   * RequestStateContext} instance are removed from {@link #threadLocalRequestStateProviders}.
   *
   * <p>If no {@link RequestStateProvider}s remain in {@link #threadLocalRequestStateProviders},
   * {@link #threadLocalRequestStateProviders} is unset.
   */
  @Override
  public void close() {
    if (threadLocalRequestStateProviders.get() != null) {
      requestStateProviders.forEach(
          requestStateProvider ->
              threadLocalRequestStateProviders.get().remove(requestStateProvider));
      if (threadLocalRequestStateProviders.get().isEmpty()) {
        threadLocalRequestStateProviders.remove();
      }
    }
  }

  /**
   * Context for running a non-cancellable operation.
   *
   * <p>While open, the current request cannot be cancelled.
   */
  public interface NonCancellableOperationContext extends AutoCloseable {
    @Override
    void close();
  }
}
