package org.frameworkset.web.servlet;

import java.io.IOException;
import java.security.Principal;
import java.util.Locale;
import java.util.concurrent.Callable;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;

import org.apache.log4j.Logger;
import org.frameworkset.http.CorsUtils;
import org.frameworkset.spi.support.LocaleContext;
import org.frameworkset.spi.support.LocaleContextHolder;
import org.frameworkset.spi.support.SimpleLocaleContext;
import org.frameworkset.util.ClassUtils;
import org.frameworkset.util.annotations.HttpMethod;
import org.frameworkset.web.request.async.CallableProcessingInterceptorAdapter;
import org.frameworkset.web.request.async.WebAsyncManager;
import org.frameworkset.web.request.async.WebAsyncUtils;
import org.frameworkset.web.servlet.context.RequestAttributes;
import org.frameworkset.web.servlet.context.RequestContextHolder;
import org.frameworkset.web.servlet.context.ServletRequestAttributes;
import org.frameworkset.web.servlet.mvc.NativeWebRequest;

import com.frameworkset.util.StringUtil;

public abstract class BaseServlet extends HttpServlet{
	/** Checking for Servlet 3.0+ HttpServletResponse.getStatus() */
	private static final boolean responseGetStatusAvailable =
			ClassUtils.hasMethod(HttpServletResponse.class, "getStatus");

	/** Logger available to subclasses */
	protected static final Logger logger = Logger.getLogger(BaseServlet.class);
	
	/** Should we dispatch an HTTP TRACE request to {@link #doService}? */
	private boolean dispatchTraceRequest = false;
	/** Expose LocaleContext and RequestAttributes as inheritable for child threads? */
	private static boolean threadContextInheritable = false;
	/**
	 * Close the WebApplicationContext of this servlet.
	 * @see org.frameworkset.context.ConfigurableApplicationContext#close()
	 */
	@Override
	public void destroy() {
//		getServletContext().log("Destroying Spring FrameworkServlet '" + getServletName() + "'");
//		// Only call close() on WebApplicationContext if locally managed...
//		if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
//			((ConfigurableApplicationContext) this.webApplicationContext).close();
//		}
	}


	/**
	 * Override the parent class implementation in order to intercept PATCH requests.
	 */
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		if (HttpMethod.PATCH.matches(request.getMethod())) {
			processRequest(request, response);
		}
		else {
			super.service(request, response);
		}
	}

	/**
	 * Delegate GET requests to processRequest/doService.
	 * <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
	 * with a {@code NoBodyResponse} that just captures the content length.
	 * @see #doService
	 * @see #doHead
	 */
	@Override
	protected final void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		processRequest(request, response);
	}

	/**
	 * Delegate POST requests to {@link #processRequest}.
	 * @see #doService
	 */
	@Override
	protected final void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		processRequest(request, response);
	}

	/**
	 * Delegate PUT requests to {@link #processRequest}.
	 * @see #doService
	 */
	@Override
	protected final void doPut(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		processRequest(request, response);
	}

	/**
	 * Delegate DELETE requests to {@link #processRequest}.
	 * @see #doService
	 */
	@Override
	protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		processRequest(request, response);
	}
	/** Should we dispatch an HTTP OPTIONS request to {@link #doService}? */
	private boolean dispatchOptionsRequest = false;
	/**
	 * Delegate OPTIONS requests to {@link #processRequest}, if desired.
	 * <p>Applies HttpServlet's standard OPTIONS processing otherwise,
	 * and also if there is still no 'Allow' header set after dispatching.
	 * @see #doService
	 */
	@Override
	protected void doOptions(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
			processRequest(request, response);
			if (response.containsHeader("Allow")) {
				// Proper OPTIONS response coming from a handler - we're done.
				return;
			}
		}

		// Use response wrapper for Servlet 2.5 compatibility where
		// the getHeader() method does not exist
		super.doOptions(request, new HttpServletResponseWrapper(response) {
			@Override
			public void setHeader(String name, String value) {
				if ("Allow".equals(name)) {
					value = (StringUtil.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
				}
				super.setHeader(name, value);
			}
		});
	}

	/**
	 * Delegate TRACE requests to {@link #processRequest}, if desired.
	 * <p>Applies HttpServlet's standard TRACE processing otherwise.
	 * @see #doService
	 */
	@Override
	protected void doTrace(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		if (this.dispatchTraceRequest) {
			processRequest(request, response);
			if ("message/http".equals(response.getContentType())) {
				// Proper TRACE response coming from a handler - we're done.
				return;
			}
		}
		super.doTrace(request, response);
	}
	/** LocaleResolver used by this servlet */
	protected static LocaleResolver localeResolver;
	/**
	 * Build a LocaleContext for the given request, exposing the request's
	 * primary locale as current locale.
	 * <p>The default implementation uses the dispatcher's LocaleResolver
	 * to obtain the current locale, which might change during a request.
	 * @param request current HTTP request
	 * @return the corresponding LocaleContext
	 */
	protected static LocaleContext buildLocaleContext(final HttpServletRequest request) {
		Locale locale = localeResolver.resolveLocale(request);
		return new SimpleLocaleContext(locale);
	}
//	/**
//	 * Build a LocaleContext for the given request, exposing the request's
//	 * primary locale as current locale.
//	 * @param request current HTTP request
//	 * @return the corresponding LocaleContext, or {@code null} if none to bind
//	 * @see LocaleContextHolder#setLocaleContext
//	 */
//	protected LocaleContext buildLocaleContext(HttpServletRequest request) {
//		return new SimpleLocaleContext(request.getLocale());
//	}
	/**
	 * Build ServletRequestAttributes for the given request (potentially also
	 * holding a reference to the response), taking pre-bound attributes
	 * (and their type) into consideration.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @param previousAttributes pre-bound RequestAttributes instance, if any
	 * @return the ServletRequestAttributes to bind, or {@code null} to preserve
	 * the previously bound instance (or not binding any, if none bound before)
	 * @see RequestContextHolder#setRequestAttributes
	 */
	protected ServletRequestAttributes buildRequestAttributes(
			HttpServletRequest request, HttpServletResponse response, PageContext pageContext,RequestAttributes previousAttributes) {

		if (previousAttributes == null || previousAttributes instanceof ServletRequestAttributes) {
			return new ServletRequestAttributes(request, response,pageContext);
		}
		else {
			return null;  // preserve the pre-bound RequestAttributes instance
		}
	}
	private void initContextHolders(
			HttpServletRequest request, LocaleContext localeContext, RequestAttributes requestAttributes) {

		if (localeContext != null) {
			LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
		}
		if (requestAttributes != null) {
			RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Bound request context to thread: " + request);
		}
	}

	private void resetContextHolders(HttpServletRequest request,
			LocaleContext prevLocaleContext, RequestAttributes previousAttributes) {

		LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
		RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
		if (logger.isTraceEnabled()) {
			logger.trace("Cleared thread-bound request context: " + request);
		}
	}
	/**
	 * Process this request, publishing an event regardless of the outcome.
	 * <p>The actual event handling is performed by the abstract
	 * {@link #doService} template method.
	 */
	protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		long startTime = System.currentTimeMillis();
		Throwable failureCause = null;
//		LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable);
//		setLocaleContext(  request);

		// Expose current RequestAttributes to current thread.
//		previousRequestAttributes = RequestContextHolder.getRequestAttributes();
		PageContext pageContext = null;
		JspFactory fac= null;
		fac=JspFactory.getDefaultFactory();
		pageContext=fac.getPageContext(this, request,response, null, false, JspWriter.DEFAULT_BUFFER <= 0?8192:JspWriter.DEFAULT_BUFFER, true); 
//		requestAttributes = new ServletRequestAttributes(request, response,pageContext);
//		RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
		
		LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
		LocaleContext localeContext = buildLocaleContext(request);

		RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, pageContext,previousAttributes);

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
		asyncManager.registerCallableInterceptor(BaseServlet.class.getName(), new RequestBindingInterceptor(pageContext));

		initContextHolders(request, localeContext, requestAttributes);

		try {
			doService(request, response);
		}
		catch (ServletException ex) {
			failureCause = ex;
			throw ex;
		}
		catch (IOException ex) {
			failureCause = ex;
			throw ex;
		}
		catch (Throwable ex) {
			failureCause = ex;
			throw new NestedServletException("Request processing failed", ex);
		}

		finally {
			resetContextHolders(request, previousLocaleContext, previousAttributes);
			if (requestAttributes != null) {
				requestAttributes.requestCompleted();
			}

			if (logger.isDebugEnabled()) {
				if (failureCause != null) {
					this.logger.debug("Could not complete request", failureCause);
				}
				else {
					if (asyncManager.isConcurrentHandlingStarted()) {
						logger.debug("Leaving response open for concurrent processing");
					}
					else {
						this.logger.debug("Successfully completed request");
					}
				}
			}
			if(fac != null && pageContext != null)
			{
				fac.releasePageContext(pageContext);
			}

//			publishRequestHandledEvent(request, response, startTime, failureCause);
		}
	}
	
//	private void publishRequestHandledEvent(
//			HttpServletRequest request, HttpServletResponse response, long startTime, Throwable failureCause) {
//
//		if (this.publishEvents) {
//			// Whether or not we succeeded, publish an event.
//			long processingTime = System.currentTimeMillis() - startTime;
//			int statusCode = (responseGetStatusAvailable ? response.getStatus() : -1);
//			this.webApplicationContext.publishEvent(
//					new ServletRequestHandledEvent(this,
//							request.getRequestURI(), request.getRemoteAddr(),
//							request.getMethod(), getServletConfig().getServletName(),
//							WebUtils.getSessionId(request), getUsernameForRequest(request),
//							processingTime, failureCause, statusCode));
//		}
//	}

	/**
	 * Determine the username for the given request.
	 * <p>The default implementation takes the name of the UserPrincipal, if any.
	 * Can be overridden in subclasses.
	 * @param request current HTTP request
	 * @return the username, or {@code null} if none found
	 * @see javax.servlet.http.HttpServletRequest#getUserPrincipal()
	 */
	protected String getUsernameForRequest(HttpServletRequest request) {
		Principal userPrincipal = request.getUserPrincipal();
		return (userPrincipal != null ? userPrincipal.getName() : null);
	}


	/**
	 * Subclasses must implement this method to do the work of request handling,
	 * receiving a centralized callback for GET, POST, PUT and DELETE.
	 * <p>The contract is essentially the same as that for the commonly overridden
	 * {@code doGet} or {@code doPost} methods of HttpServlet.
	 * <p>This class intercepts calls to ensure that exception handling and
	 * event publication takes place.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception in case of any kind of processing failure
	 * @see javax.servlet.http.HttpServlet#doGet
	 * @see javax.servlet.http.HttpServlet#doPost
	 */
	protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
			throws Exception;


	

	/**
	 * CallableProcessingInterceptor implementation that initializes and resets
	 * FrameworkServlet's context holders, i.e. LocaleContextHolder and RequestContextHolder.
	 */
	private class RequestBindingInterceptor extends CallableProcessingInterceptorAdapter {
		private PageContext pageContext;
		public RequestBindingInterceptor(PageContext pageContext)
		{
			this.pageContext =  pageContext;
		}
		@Override
		public <T> void preProcess(NativeWebRequest webRequest, Callable<T> task) {
			HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
			if (request != null) {
				HttpServletResponse response = webRequest.getNativeRequest(HttpServletResponse.class);
				initContextHolders(request, buildLocaleContext(request), buildRequestAttributes(request, response,pageContext, null));
			}
		}
		@Override
		public <T> void postProcess(NativeWebRequest webRequest, Callable<T> task, Object concurrentResult) {
			HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
			if (request != null) {
				resetContextHolders(request, null, null);
			}
		}
	}

}
