/*
 * Copyright 2013-2021 the original author or 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
 *
 *      https://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.springframework.cloud.sleuth.instrument.web.servlet;

import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.http.HttpServerHandler;
import org.springframework.cloud.sleuth.http.HttpServerResponse;

/**
 * Access to servlet version-specific features.
 *
 * <p>
 * Originally designed by OkHttp team, derived from
 * {@code okhttp3.internal.platform.Platform}
 */
abstract class ServletRuntime {

	private static final ServletRuntime SERVLET_RUNTIME = findServletRuntime();

	public HttpServletResponse httpServletResponse(ServletResponse response) {
		return (HttpServletResponse) response;
	}

	/**
	 * public for
	 * {@link org.springframework.cloud.sleuth.instrument.web.servlet.HttpServletResponseWrapper}.
	 */
	public abstract int status(HttpServletResponse response);

	public abstract boolean isAsync(HttpServletRequest request);

	public abstract void handleAsync(HttpServerHandler handler, HttpServletRequest request,
			HttpServletResponse response, Span span);

	ServletRuntime() {
	}

	public static ServletRuntime get() {
		return SERVLET_RUNTIME;
	}

	/** Attempt to match the host runtime to a capable Platform implementation. */
	private static ServletRuntime findServletRuntime() {
		// Find Servlet v3 new methods
		try {
			Class.forName("javax.servlet.AsyncEvent");
			HttpServletRequest.class.getMethod("isAsyncStarted");
			return new Servlet3(); // intentionally doesn't not access the type prior to
									// the above guard
		}
		catch (NoSuchMethodException e) {
			// pre Servlet v3
		}
		catch (ClassNotFoundException e) {
			// pre Servlet v3
		}

		throw new UnsupportedOperationException("Unsupported Servlet type");
	}

	// Taken from RxJava throwIfFatal, which was taken from scala
	public static void propagateIfFatal(Throwable t) {
		if (t instanceof VirtualMachineError) {
			throw (VirtualMachineError) t;
		}
		else if (t instanceof ThreadDeath) {
			throw (ThreadDeath) t;
		}
		else if (t instanceof LinkageError) {
			throw (LinkageError) t;
		}
	}

	static final class Servlet3 extends ServletRuntime {

		@Override
		public boolean isAsync(HttpServletRequest request) {
			return request.isAsyncStarted();
		}

		@Override
		public int status(HttpServletResponse response) {
			return response.getStatus();
		}

		@Override
		public void handleAsync(HttpServerHandler handler, HttpServletRequest request, HttpServletResponse response,
				Span span) {
			if (span.isNoop()) {
				return; // don't add overhead when we aren't httpTracing
			}
			TracingAsyncListener listener = new TracingAsyncListener(handler, span);
			request.getAsyncContext().addListener(listener, request, response);
		}

		static final class TracingAsyncListener implements AsyncListener {

			final HttpServerHandler handler;

			final Span span;

			TracingAsyncListener(HttpServerHandler handler, Span span) {
				this.handler = handler;
				this.span = span;
			}

			@Override
			public void onComplete(AsyncEvent e) {
				HttpServletRequest req = (HttpServletRequest) e.getSuppliedRequest();
				// Use package-private attribute to check if this hook was called
				// redundantly
				Object sendHandled = req.getAttribute(
						"org.springframework.cloud.sleuth.instrument.web.servlet.TracingFilter$SendHandled");
				if (sendHandled instanceof AtomicBoolean && ((AtomicBoolean) sendHandled).compareAndSet(false, true)) {
					HttpServletResponse res = (HttpServletResponse) e.getSuppliedResponse();

					HttpServerResponse response = HttpServletResponseWrapper.create(req, res, e.getThrowable());
					handler.handleSend(response, span);
				}
				else {
					// TODO: None of our tests reach this condition. Make a concrete case
					// that re-enters the
					// onComplete hook or remove the special case
				}
			}

			// Per Servlet 3 section 2.3.3.3, we can't see the final HTTP status, yet.
			// defer to onComplete
			// https://download.oracle.com/otndocs/jcp/servlet-3.0-mrel-eval-oth-JSpec/
			@Override
			public void onTimeout(AsyncEvent e) {
				// Propagate the timeout so that the onComplete hook can see it.
				ServletRequest request = e.getSuppliedRequest();
				if (request.getAttribute("error") == null) {
					request.setAttribute("error", new AsyncTimeoutException(e));
				}
			}

			// Per Servlet 3 section 2.3.3.3, we can't see the final HTTP status, yet.
			// defer to onComplete
			// https://download.oracle.com/otndocs/jcp/servlet-3.0-mrel-eval-oth-JSpec/
			@Override
			public void onError(AsyncEvent e) {
				ServletRequest request = e.getSuppliedRequest();
				if (request.getAttribute("error") == null) {
					request.setAttribute("error", e.getThrowable());
				}
			}

			/**
			 * If another async is created (ex via asyncContext.dispatch), this needs to
			 * be re-attached.
			 */
			@Override
			public void onStartAsync(AsyncEvent e) {
				AsyncContext eventAsyncContext = e.getAsyncContext();
				if (eventAsyncContext != null) {
					eventAsyncContext.addListener(this, e.getSuppliedRequest(), e.getSuppliedResponse());
				}
			}

			@Override
			public String toString() {
				return "TracingAsyncListener{" + span + "}";
			}

		}

		/**
		 * Async timeout exception.
		 */
		static final class AsyncTimeoutException extends TimeoutException {

			AsyncTimeoutException(AsyncEvent e) {
				super("Timed out after " + e.getAsyncContext().getTimeout() + "ms");
			}

			@Override
			public Throwable fillInStackTrace() {
				return this; // stack trace doesn't add value as this is used in a
								// callback
			}

		}

	}

}
