// ***************************************************************************************************************************
// * 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.juneau.rest;

import static java.util.logging.Level.*;
import static javax.servlet.http.HttpServletResponse.*;
import static org.apache.juneau.internal.IOUtils.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.io.*;
import java.util.*;

import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.vars.*;

/**
 * Default implementation of {@link RestCallHandler}.
 * 
 * <p>
 * Subclasses can override these methods to tailor how HTTP REST calls are handled.
 * <br>Subclasses MUST implement a public constructor that takes in a {@link RestContext} object.
 * 
 * <h5 class='section'>See Also:</h5>
 * <ul>
 * 	<li class='jf'>{@link RestContext#REST_callHandler}
 * </ul>
 */
public class BasicRestCallHandler implements RestCallHandler {

	private final RestContext context;
	private final RestLogger logger;
	private final Map<String,RestCallRouter> restCallRouters;

	/**
	 * Constructor.
	 * 
	 * @param context The resource context.
	 */
	public BasicRestCallHandler(RestContext context) {
		this.context = context;
		this.logger = context.getLogger();
		this.restCallRouters = context.getCallRouters();
	}

	/**
	 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object.
	 * 
	 * <p>
	 * Subclasses may choose to override this method to provide a specialized request object.
	 * 
	 * @param req The request object from the {@link #service(HttpServletRequest, HttpServletResponse)} method.
	 * @return The wrapped request object.
	 * @throws ServletException If any errors occur trying to interpret the request.
	 */
	@Override /* RestCallHandler */
	public RestRequest createRequest(HttpServletRequest req) throws ServletException {
		return new RestRequest(context, req);
	}

	/**
	 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object
	 * and the request returned by {@link #createRequest(HttpServletRequest)}.
	 * 
	 * <p>
	 * Subclasses may choose to override this method to provide a specialized response object.
	 * 
	 * @param req The request object returned by {@link #createRequest(HttpServletRequest)}.
	 * @param res The response object from the {@link #service(HttpServletRequest, HttpServletResponse)} method.
	 * @return The wrapped response object.
	 * @throws ServletException If any errors occur trying to interpret the request or response.
	 */
	@Override /* RestCallHandler */
	public RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException {
		return new RestResponse(context, req, res);
	}

	/**
	 * The main service method.
	 * 
	 * <p>
	 * Subclasses can optionally override this method if they want to tailor the behavior of requests.
	 * 
	 * @param r1 The incoming HTTP servlet request object.
	 * @param r2 The incoming HTTP servlet response object.
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override /* RestCallHandler */
	public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException {

		logger.log(FINE, "HTTP: {0} {1}", r1.getMethod(), r1.getRequestURI());
		long startTime = System.currentTimeMillis();

		try {
			context.checkForInitException();

			String pathInfo = RestUtils.getPathInfoUndecoded(r1);  // Can't use r1.getPathInfo() because we don't want '%2F' resolved.

			// If this resource has child resources, try to recursively call them.
			if (pathInfo != null && context.hasChildResources() && (! pathInfo.equals("/"))) {
				int i = pathInfo.indexOf('/', 1);
				String pathInfoPart = i == -1 ? pathInfo.substring(1) : pathInfo.substring(1, i);
				RestContext childResource = context.getChildResource(pathInfoPart);
				if (childResource != null) {
					final String pathInfoRemainder = (i == -1 ? null : pathInfo.substring(i));
					final String servletPath = r1.getServletPath() + "/" + pathInfoPart;
					final HttpServletRequest childRequest = new HttpServletRequestWrapper(r1) {
						@Override /* ServletRequest */
						public String getPathInfo() {
							return urlDecode(pathInfoRemainder);
						}
						@Override /* ServletRequest */
						public String getServletPath() {
							return servletPath;
						}
					};
					childResource.getCallHandler().service(childRequest, r2);
					return;
				}
			}

			context.startCall(r1, r2);

			RestRequest req = createRequest(r1);
			RestResponse res = createResponse(req, r2);
			String method = req.getMethod();
			String methodUC = method.toUpperCase(Locale.ENGLISH);

			StreamResource r = null;
			if (pathInfo != null) {
				String p = pathInfo.substring(1);
				if (context.isStaticFile(p))
					r = context.resolveStaticFile(p);
				else if (p.equals("favicon.ico"))
					res.setOutput(null);
			}

			if (r != null) {
				res.setStatus(SC_OK);
				res.setOutput(r);
			} else {
				// If the specified method has been defined in a subclass, invoke it.
				int rc = SC_METHOD_NOT_ALLOWED;
				if (restCallRouters.containsKey(methodUC)) {
					rc = restCallRouters.get(methodUC).invoke(pathInfo, req, res);
				} else if (restCallRouters.containsKey("*")) {
					rc = restCallRouters.get("*").invoke(pathInfo, req, res);
				}

				// If not invoked above, see if it's an OPTIONs request
				if (rc != SC_OK)
					handleNotFound(rc, req, res);
			}

			if (res.hasOutput()) {
				Object output = res.getOutput();

				// Do any class-level transforming.
				for (RestConverter converter : context.getConverters())
					output = converter.convert(req, output);

				res.setOutput(output);

				// Now serialize the output if there was any.
				// Some subclasses may write to the OutputStream or Writer directly.
				handleResponse(req, res, output);
			}

			// Make sure our writer in RestResponse gets written.
			res.flushBuffer();
			req.close();
			
			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);

		} catch (RestException e) {
			r1.setAttribute("Exception", e);
			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
			handleError(r1, r2, e);
		} catch (Throwable e) {
			RestException e2 = new RestException(SC_INTERNAL_SERVER_ERROR, e);
			r1.setAttribute("Exception", e);
			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
			handleError(r1, r2, e2);
		}

		context.finishCall(r1, r2);

		logger.log(FINE, "HTTP: [{0} {1}] finished in {2}ms", r1.getMethod(), r1.getRequestURI(), System.currentTimeMillis()-startTime);
	}

	/**
	 * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or
	 * returned by the Java method.
	 * 
	 * <p>
	 * Subclasses may override this method if they wish to modify the way the output is rendered or support other output
	 * formats.
	 * 
	 * <p>
	 * The default implementation simply iterates through the response handlers on this resource
	 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse, Object)} method returns
	 * <jk>true</jk>.
	 * 
	 * @param req The HTTP request.
	 * @param res The HTTP response.
	 * @param output The output to serialize in the response.
	 * @throws IOException
	 * @throws RestException
	 */
	@Override /* RestCallHandler */
	public void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException {
		// Loop until we find the correct handler for the POJO.
		for (ResponseHandler h : context.getResponseHandlers())
			if (h.handle(req, res, output))
				return;
		throw new RestException(SC_NOT_IMPLEMENTED, "No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'");
	}

	/**
	 * Handle the case where a matching method was not found.
	 * 
	 * <p>
	 * Subclasses can override this method to provide a 2nd-chance for specifying a response.
	 * The default implementation will simply throw an exception with an appropriate message.
	 * 
	 * @param rc The HTTP response code.
	 * @param req The HTTP request.
	 * @param res The HTTP response.
	 * @throws Exception
	 */
	@Override /* RestCallHandler */
	public void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception {
		String pathInfo = req.getPathInfo();
		String methodUC = req.getMethod();
		String onPath = pathInfo == null ? " on no pathInfo"  : String.format(" on path '%s'", pathInfo);
		if (rc == SC_NOT_FOUND)
			throw new RestException(rc, "Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath);
		else if (rc == SC_PRECONDITION_FAILED)
			throw new RestException(rc, "Method ''{0}'' not found on resource{1} with matching matcher.", methodUC, onPath);
		else if (rc == SC_METHOD_NOT_ALLOWED)
			throw new RestException(rc, "Method ''{0}'' not found on resource.", methodUC);
		else
			throw new ServletException("Invalid method response: " + rc);
	}

	/**
	 * Method for handling response errors.
	 * 
	 * <p>
	 * The default implementation logs the error and calls
	 * {@link #renderError(HttpServletRequest,HttpServletResponse,RestException)}.
	 * 
	 * <p>
	 * Subclasses can override this method to provide their own custom error response handling.
	 * 
	 * @param req The servlet request.
	 * @param res The servlet response.
	 * @param e The exception that occurred.
	 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
	 */
	@Override /* RestCallHandler */
	public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException {
		e.setOccurrence(context == null ? 0 : context.getStackTraceOccurrence(e));
		logger.onError(req, res, e);
		renderError(req, res, e);
	}

	/**
	 * Method for rendering response errors.
	 * 
	 * <p>
	 * The default implementation renders a plain text English message, optionally with a stack trace if
	 * {@link RestResource#renderResponseStackTraces() @RestResource.renderResponseStackTraces()} is enabled.
	 * 
	 * <p>
	 * Subclasses can override this method to provide their own custom error response handling.
	 * 
	 * @param req The servlet request.
	 * @param res The servlet response.
	 * @param e The exception that occurred.
	 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
	 */
	@Override /* RestCallHandler */
	public void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException {

		int status = e.getStatus();
		res.setStatus(status);
		res.setContentType("text/plain");
		res.setHeader("Content-Encoding", "identity");

		Throwable t = e.getRootCause();
		if (t != null) {
			res.setHeader("Exception-Name", t.getClass().getName());
			res.setHeader("Exception-Message", t.getMessage());
		}

		PrintWriter w = null;
		try {
			w = res.getWriter();
		} catch (IllegalStateException e2) {
			w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8));
		}
		
		try (PrintWriter w2 = w) {
			String httpMessage = RestUtils.getHttpResponseText(status);
			if (httpMessage != null)
				w2.append("HTTP ").append(String.valueOf(status)).append(": ").append(httpMessage).append("\n\n");
			if (context != null && context.isRenderResponseStackTraces())
				e.printStackTrace(w2);
			else
				w2.append(e.getFullStackMessage(true));
		}
	}

	/**
	 * Returns the session objects for the specified request.
	 * 
	 * <p>
	 * The default implementation simply returns a single map containing <code>{'req':req}</code>.
	 * 
	 * @param req The REST request.
	 * @return The session objects for that request.
	 */
	@Override /* RestCallHandler */
	public Map<String,Object> getSessionObjects(RestRequest req) {
		Map<String,Object> m = new HashMap<>();
		m.put(RequestVar.SESSION_req, req);
		return m;
	}
}
