/*******************************************************************************
 * Copyright (c) 2009, 2018 Cloudsmith Inc and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *  IBM Corporation - initial API and implementation
 *  Cloudsmith Inc. - this copy and adaption to abstract servlet
 *******************************************************************************/
package org.eclipse.equinox.p2.testserver.servlets;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.equinox.p2.testserver.Activator;
import org.eclipse.equinox.p2.testserver.HttpConstants;
import org.eclipse.equinox.p2.testserver.MimeLookup;
import org.eclipse.equinox.p2.testserver.SecureAction;
import org.osgi.service.http.HttpService;

public class BasicResourceDelivery extends HttpServlet {
	private static final long serialVersionUID = 1L;
	// private static final String CHARSET = "utf-8"; //$NON-NLS-1$
	private final SecureAction secureAction;
	private final String alias;
	private final URI path;
	protected static final String defaultMimeType = "application/octet-stream"; //$NON-NLS-1$

	/**
	 * Delivers resources from the bundle, or from an absolute URI
	 */
	public BasicResourceDelivery(String theAlias, URI thePath) {
		secureAction = new SecureAction();
		alias = theAlias;
		path = thePath;
	}

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		URI resource = null;
		try {
			resource = getFilename(request.getRequestURI());
		} catch (URISyntaxException e) {
			/* ignore - just leave resource == null */
		}

		URL url = null;
		if (resource != null) {
			if (!resource.isAbsolute())
				url = getServletContext().getResource(resource.getPath());
			else
				url = resource.toURL();
		}

		if (url == null) {
			fileNotFound(resource, response);
			return;
		}

		// process cache control
		URLConnection conn = secureAction.openURL(url);

		long modifiedSince = request.getDateHeader("If-Modified-Since"); //$NON-NLS-1$
		if (modifiedSince >= 0) {
			long modified = getLastModified(conn);
			if ((modified > 0) && (modifiedSince >= modified)) {
				response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
				return;
			}
		}
		InputStream in;
		try {
			in = conn.getInputStream();
		} catch (IOException ex) {
			fileNotFound(resource, response);
			return;
		}
		try {
			// always set the default charset
			// not in Servlet 2.1 // response.setCharacterEncoding(CHARSET);
			deliver(conn, in, resource.toString(), request, response); // TODO: modify to resource==URI ?
		} finally {
			in.close();
		}
	}

	protected void deliver(URLConnection conn, InputStream in, String filename, HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		this.doDeliver(conn, in, filename, request, response);
	}

	/**
	 * Default method delivering content from a file. Subclasses should override
	 * this method to deliver "broken content" (e.g. truncated, mismatched content
	 * length etc.). This default method performs the same delivery as the default
	 * "register resource" in the http service.
	 *
	 * @param conn     - The URLConnection to the resource
	 * @param in       - InputStream to read from
	 * @param filename - the filename being read
	 * @param request  - the servlet request
	 * @param response - the servlet response
	 * @throws IOException - on errors
	 */
	protected void doDeliver(URLConnection conn, InputStream in, String filename, HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		// set when the resource was modified
		addDateHeader(response, HttpConstants.LAST_MODIFIED, getLastModified(conn));
		int statusCode = HttpHeaderToStatus(conn.getHeaderField(0));

		response.setStatus(statusCode != -1 ? HttpServletResponse.SC_OK : statusCode);

		int contentlength = getContentLength(conn);
		if (contentlength >= 0) {
			response.setContentLength(contentlength);

			String mimeType = computeMimeType(filename, conn);
			response.setContentType(mimeType);

			// We want to use a writer if we are sending text
			if (mimeType.startsWith("text/")) //$NON-NLS-1$
			{
				PrintWriter writer = response.getWriter();

				writer.flush(); /* write the headers and unbuffer the output */

				BufferedReader reader = new BufferedReader(new InputStreamReader(in));

				char buffer[] = new char[4096];
				int read;
				while ((read = reader.read(buffer, 0, buffer.length)) != -1) {
					writer.write(buffer, 0, read);
				}
			} else {
				ServletOutputStream out = response.getOutputStream();

				out.flush(); /* write the headers and unbuffer the output */

				byte buffer[] = new byte[4096];
				int read;
				while ((read = in.read(buffer, 0, buffer.length)) != -1) {
					out.write(buffer, 0, read);
					out.flush();
				}
			}
		}
	}

	protected void fileNotFound(URI file, HttpServletResponse response) throws IOException {
		response.sendError(HttpServletResponse.SC_NOT_FOUND,
				(file == null ? "<no file>" : file.toString()) + " not found"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	@Override
	protected void doHead(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		URI resource = null;
		try {
			resource = getFilename(request.getRequestURI());
		} catch (URISyntaxException e) {
			/* ignore - just leave resource == null */
		}

		URL url = null;
		if (resource != null) {
			if (!resource.isAbsolute())
				url = getServletContext().getResource(resource.getPath());
			else
				url = resource.toURL();
		}

		if (url == null) {
			fileNotFound(resource, response);
			return;
		}
		URLConnection conn = secureAction.openURL(url);
		// always set default charset
		// Not in Servlet 2.1 // response.setCharacterEncoding(CHARSET);
		doDeliverHead(resource.toString(), conn, request, response); // TODO: change API to use URI?
	}

	protected void deliverHead(String filename, URLConnection conn, HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		this.doDeliverHead(filename, conn, request, response);
	}

	protected void doDeliverHead(String filename, URLConnection conn, HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		int contentlength = getContentLength(conn);
		int statusCode = HttpHeaderToStatus(conn.getHeaderField(0));
		// set status = ok if there was no http header...
		response.setStatus(statusCode == -1 ? HttpServletResponse.SC_OK : statusCode);
		if (contentlength >= 0) {
			response.setContentLength(contentlength);

			String mimeType = computeMimeType(filename, conn);
			response.setContentType(mimeType);
			long modified = getLastModified(conn);
			addDateHeader(response, HttpConstants.LAST_MODIFIED, modified);
		} else {
			super.doHead(request, response);
		}

	}

	public int HttpHeaderToStatus(String httpHeader) {
		if (httpHeader == null)
			return -1;
		try {
			return Integer.valueOf((httpHeader.substring(9, 12))).intValue();
		} catch (IndexOutOfBoundsException e) {
			return -1;
		} catch (NumberFormatException e) {
			return -1;
		}
	}

	/**
	 * Override to lie about last modified
	 *
	 * @param conn
	 * @return time in ms
	 */
	protected long getLastModified(URLConnection conn) {
		return conn.getLastModified();
	}

	/**
	 * Override to lie about content length
	 *
	 * @param conn
	 * @return length in bytes
	 */
	protected int getContentLength(URLConnection conn) {
		return conn.getContentLength();
	}

	protected URI getFilename(String filename) throws URISyntaxException {
		// If the requested URI is equal to the Registeration's alias, send the file
		// corresponding to the alias. Otherwise, we have request for a file in an
		// registered directory (the file was not directly registered itself).
		if (filename.equals(alias)) {
			filename = path.getPath();
		} else {
			// The file we re looking for is the registered resource (alias) + the rest of
			// the
			// filename that is not part of the registered resource. For example, if we
			// export
			// /a to /tmp and we have a request for /a/b/foo.txt, then /tmp is our directory
			// (file.toString()) and /b/foo.txt is the rest.
			// The result is that we open the file /tmp/b/foo.txt.

			int aliaslen = alias.length();
			int pathlen = path.getPath().length();

			if (pathlen == 1) /* path == "/" */
			{
				if (aliaslen > 1) /* alias != "/" */
				{
					filename = filename.substring(aliaslen);
				}
			} else /* path != "/" */
			{
				StringBuilder buf = new StringBuilder(aliaslen + pathlen);
				buf.append(path.getPath());

				if (aliaslen == 1) /* alias == "/" */
				{
					buf.append(filename);
				} else /* alias != "/" */
				{
					buf.append(filename.substring(aliaslen));
				}

				filename = buf.toString();
			}
		}
		return new URI(path.getScheme(), path.getUserInfo(), path.getHost(), path.getPort(), filename, path.getQuery(),
				path.getFragment());
		// return (filename);
	}

	/**
	 * This method returns the correct MIME type of a given URI by first checking
	 * the HttpContext::getMimeType and, if null, checking the httpservice's
	 * MIMETypes table. return mimetype with charset=utf-8 for all text/... types
	 */
	protected String computeMimeType(String name, URLConnection conn) {
		String mimeType = computeMimeType2(name, conn);
		if (mimeType.startsWith("text/")) //$NON-NLS-1$
			return mimeType + "; charset=utf-8"; //$NON-NLS-1$
		return mimeType;
	}

	protected String computeMimeType2(String name, URLConnection conn) {
		// use type set in connection if it is available
		String mimeType = conn.getContentType();
		if (mimeType != null) {
			return (mimeType);
		}
		// use type from context
		mimeType = getServletContext().getMimeType(name);
		if (mimeType != null) {
			return (mimeType);
		}
		// try the "extras"
		return MimeLookup.getMimeType(name);
	}

	public HttpService getHttpService() {
		return Activator.getInstance().getHttp();
	}

	public static final DateFormat PATTERN_RFC1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); //$NON-NLS-1$

	/**
	 * Support setting date header - in servlet > 2.1 it is possible to call this
	 * method directly on the HttpResponse.
	 *
	 * @param response
	 * @param name
	 * @param timestamp
	 */
	public void addDateHeader(HttpServletResponse response, String name, long timestamp) {
		DateFormat df = PATTERN_RFC1123;
		// must always be GMT
		df.setTimeZone(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$
		response.setHeader(name, df.format(new Date(timestamp)));

	}

}
