/*
 * Copyright 2018, OpenCensus Authors
 *
 * 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 io.opencensus.contrib.spring.sleuth.v1x;

import io.grpc.Context;
import io.opencensus.common.ExperimentalApi;
import io.opencensus.trace.ContextHandle;
import io.opencensus.trace.unsafe.ContextHandleUtils;
import org.apache.commons.logging.Log;
import org.springframework.cloud.sleuth.Span;
import org.springframework.core.NamedThreadLocal;

/*>>>
  import org.checkerframework.checker.nullness.qual.Nullable;
*/

/** Inspired by the Sleuth's {@code SpanContextHolder}. */
@ExperimentalApi
final class OpenCensusSleuthSpanContextHolder {
  private static final Log log =
      org.apache.commons.logging.LogFactory.getLog(OpenCensusSleuthSpanContextHolder.class);
  private static final ThreadLocal</*@Nullable*/ SpanContext> CURRENT_SPAN =
      new NamedThreadLocal</*@Nullable*/ SpanContext>("Trace Context");

  // Get the current span out of the thread context.
  @javax.annotation.Nullable
  static Span getCurrentSpan() {
    SpanContext currentSpanContext = CURRENT_SPAN.get();
    return currentSpanContext != null ? currentSpanContext.span : null;
  }

  // Set the current span in the thread context
  static void setCurrentSpan(Span span) {
    if (log.isTraceEnabled()) {
      log.trace("Setting current span " + span);
    }
    push(span, /* autoClose= */ false);
  }

  // Remove all thread context relating to spans (useful for testing).
  // See close() for a better alternative in instrumetation
  static void removeCurrentSpan() {
    removeCurrentSpanInternal(null);
  }

  @SuppressWarnings("CheckReturnValue")
  @javax.annotation.Nullable
  private static SpanContext removeCurrentSpanInternal(
      @javax.annotation.Nullable SpanContext toRestore) {
    if (toRestore != null) {
      setSpanContextInternal(toRestore);
    } else {
      CURRENT_SPAN.remove();
      // This is a big hack and can cause other data in the io.grpc.Context to be lost. But
      // Spring 1.5 does not use io.grpc.Context and because the framework does not accept any
      // gRPC context, the context will always be ROOT anyway.
      Context.ROOT.attach();
    }
    return toRestore;
  }

  // Check if there is already a span in the current thread.
  static boolean isTracing() {
    return CURRENT_SPAN.get() != null;
  }

  // Close the current span and all parents that can be auto closed. On every iteration a function
  // will be applied on the closed Span.
  static void close(SpanFunction spanFunction) {
    SpanContext current = CURRENT_SPAN.get();
    while (current != null) {
      spanFunction.apply(current.span);
      current = removeCurrentSpanInternal(current.parent);
      if (current == null || !current.autoClose) {
        return;
      }
    }
  }

  // Close the current span and all parents that can be auto closed.
  static void close() {
    close(NO_OP_FUNCTION);
  }

  /**
   * Push a span into the thread context, with the option to have it auto close if any child spans
   * are themselves closed. Use autoClose=true if you start a new span with a parent that wasn't
   * already in thread context.
   */
  static void push(Span span, boolean autoClose) {
    if (isCurrent(span)) {
      return;
    }
    setSpanContextInternal(new SpanContext(span, autoClose));
  }

  interface SpanFunction {
    void apply(Span span);
  }

  private static final SpanFunction NO_OP_FUNCTION =
      new SpanFunction() {
        @Override
        public void apply(Span span) {}
      };

  @SuppressWarnings("CheckReturnValue")
  private static void setSpanContextInternal(SpanContext spanContext) {
    CURRENT_SPAN.set(spanContext);
    spanContext.ocCurrentContext.attach();
  }

  private static boolean isCurrent(Span span) {
    if (span == null) {
      return false;
    }
    SpanContext currentSpanContext = CURRENT_SPAN.get();
    return currentSpanContext != null && span.equals(currentSpanContext.span);
  }

  private static class SpanContext {
    final Span span;
    final boolean autoClose;
    @javax.annotation.Nullable final SpanContext parent;
    final OpenCensusSleuthSpan ocSpan;
    final ContextHandle ocCurrentContext;

    private SpanContext(Span span, boolean autoClose) {
      this.span = span;
      this.autoClose = autoClose;
      this.parent = CURRENT_SPAN.get();
      this.ocSpan = new OpenCensusSleuthSpan(span);
      this.ocCurrentContext =
          ContextHandleUtils.withValue(ContextHandleUtils.currentContext(), this.ocSpan);
    }
  }

  private OpenCensusSleuthSpanContextHolder() {}
}
