/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.http.server;

import lc.mecha.fabric.HandlerStatus;
import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;
import org.apache.commons.io.IOUtils;

import javax.activation.MimetypesFileTypeMap;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;

/**
 * <code>FileServer</code> provides a static file server for an ESP <code>WebServer</code>. The
 * files may be stored either internally (on the classpath) or externally (on the host file-system).
 * <p>
 * This version of the <code>FileServer</code> does not provide any "real" web-server features such
 * as index.html re-direction or authentication. The requests are interpreted literally and either
 * served, or not. If these features are desired, they should be provided by other handlers which
 * have been registered in the pipeline chain.
 * <p>
 * Example: <code>
 * WebServer ws = ...
 * FileServer fs = new FileServer("com.foo.webroot","/static");
 * ws.getHandlerPack().registerHandler("/static",fs);
 * </code>
 */
public abstract class FileServer extends PrefixedHandler {
    private final static MechaLogger logger = MechaLoggerFactory.getLogger(FileServer.class);
    private final MimetypesFileTypeMap mimeMap = MimeUtil.defaultMapper();
    private final String uriBase;
    private final boolean internalContent;
    private final long lastModified = System.currentTimeMillis();

    public FileServer(final Set<String> pathPrefix) {
        this(pathPrefix, true);
        mimeMap.addMimeTypes("text/css css");
        mimeMap.addMimeTypes("text/html html");
        mimeMap.addMimeTypes("application/json json");
        mimeMap.addMimeTypes("text/javascript js");
        mimeMap.addMimeTypes("image/png png");
    }

    public MimetypesFileTypeMap getMimeMap() {
        return mimeMap;
    }

    /**
     * For the given web request, return the associated content root. The content root is currently
     * specified as a classpath base (as the file server loads files via the class-loader).
     *
     * @param request
     * @return
     */
    public abstract String getContentRoot(HttpTransaction request);

    /**
     * Create a new <code>FileServer</code> using the provided class path base (the package base)
     * and the provided URI base (the root). The URI base should match up with the prefix that is
     * assigned when this <code>FileServer</code> is registered with the <code>WebServer</code> or
     * improper operation will result.
     * <p>
     * The file server can locate web content either internally (on the classpath) or externally
     * (on the filesystem). If the content is located internally, set <code>internalContent</code>
     * to <code>true</code> in which case <code>contentSource</code> should prefer to the package
     * base. If the content is located externally, set <code>contentSource</code> to the filesystem
     * root.
     * <p>
     * WARNING: The FileServer must be installed in the same prefix passed into the constructor, or string handling
     * exceptions will result. For example, do not register the handler for "/foo" but specify the prefix as "/bar".
     *
     * @param uriBase         The URI base to strip from the request.
     * @param internalContent <code>true</code> if the files are on the classpath.
     */
    private FileServer(final Set<String> uriBase, final boolean internalContent) {
        super(uriBase);
        // FIXME: This assumes the first pathPrefix is the one the request arrived on
        this.uriBase = uriBase.iterator().next();
        this.internalContent = internalContent;
    }

    public long getLastModified() {
        return lastModified;
    }

    /**
     * If the path contains a trailing slash, <code>/index.html</code> will be appended
     * to the path and returned. Otherwise, the original path will be returned.
     *
     * @param path
     * @return
     */
    // TODO: Should actually check a list of extensions for existence
    private String indexIfDirectory(final String path) {
        if (path.endsWith("/")) return path + "index.html";
        return path;
    }

    @Override
    public HandlerStatus handlePrefixedWebRequest(final HttpTransaction request) throws Throwable {
        if (request.httpServletRequest.getMethod().equals("GET")) {
            if (ServletUtil.isCached(request.httpServletRequest, getLastModified())) {
                request.httpServletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                return HandlerStatus.BREAK;
            }
        }

        final URI uri = new URI(request.httpServletRequest.getRequestURI());
        logger.debug("handlePrefixedWebRequest(): Parsed URL: {}", uri);

        String path = uri.getPath();

        // Strip uriBase from request
        path = request.request.getRequestURI().substring(uriBase.length(), path.length());
        logger.trace("Path: {}", path);

        // Security collapse path
        Path p = Paths.get(path);

        path = p.normalize().toString();
        logger.trace("Path: {}", path);

        path = indexIfDirectory(path);
        logger.trace("Indexed: {}", path);

        // Append to classPathBase
        path = getContentRoot(request) + path;
        logger.info("Finalized path: {}", path);

        InputStream is;

        if (internalContent)
            is = FileServer.class.getClassLoader().getResourceAsStream(path);
        else
            try {
                is = new FileInputStream(path);
            } catch (FileNotFoundException e) {
                logger.warn("handlePrefixedWebRequest(): File not found.", e);
                is = null;
            }

        try {
            logger.trace("handlePrefixedWebRequest(): Input Stream: {}", is);

            if (is == null) {
                logger.debug("handlePrefixedWebRequest(): Sending 404 response");
                request.httpServletResponse.setStatus(404);
                request.httpServletResponse.getOutputStream().write("404: File Not Found.".getBytes());
                return HandlerStatus.BREAK;
            }

            logger.trace("handlePrefixedWebRequest(): InputStream found is: {}", is);

            request.httpServletResponse.setContentType(mimeMap.getContentType(path));
            request.httpServletResponse.setDateHeader("Last-Modified", getLastModified());

            logger.debug("handlePrefixedWebRequest(): Content-type returning is: {}", mimeMap.getContentType(path));
            IOUtils.copy(is, request.httpServletResponse.getOutputStream());
        } finally {
            if (is != null)
                is.close();
        }

        return HandlerStatus.BREAK;
    }
}
