package de.ivu.elect.jee.servlet;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * on 28.07.2015.
 * http://stackoverflow.com/questions/132052/servlet-for-serving-static-content/29991447#29991447
 */
public abstract class StaticResourceServlet extends HttpServlet {

  private static final Logger LOG = LoggerFactory.getLogger(StaticResourceServlet.class);

  public static final StaticResource REDIRECT_INDEX = new StaticResource() {
    @Override
    @Nullable
    public String getFileName() {
      return null;
    }

    @Override
    public long getLastModified() {
      return 0;
    }

    @Override
    public long getContentLength() {
      return 0;
    }

    @Override
    @Nullable
    public InputStream getInputStream() {
      return null;
    }
  };

  private static final long serialVersionUID = 1L;

  private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);

  private static final String ETAG_HEADER = "W/\"%s-%s\"";

  private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";

  public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.MINUTES.toMillis(30);

  public static final long HTML_EXPIRE_TIME_IN_MILLIS = TimeUnit.SECONDS.toMillis(30);

  public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;

  private static final Pattern COMPILE = Pattern.compile("\\s*,\\s*");

  @Override
  protected void doHead(HttpServletRequest request, HttpServletResponse response) {
    try {
      doRequest(request, response, true);
    } catch (IOException e) {
      LOG.warn("Static resource: " + e.getMessage());
    }
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    try {
      doRequest(request, response, false);
    } catch (IOException e) {
      LOG.warn("Static resource: " + e.getMessage());
    }
  }

  private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException {
    response.reset();
    StaticResource resource;
    try {
      resource = getStaticResource(request);
    } catch (IllegalArgumentException e) {
      response.sendRedirect(request.getContextPath());
      return;
    }

    if (REDIRECT_INDEX.equals(resource)) {
      if (request.getRequestURI().endsWith("/")) {
        response.sendRedirect(request.getRequestURI() + "index.html");
      } else {
        response.sendRedirect(request.getRequestURI() + "/index.html");
      }
      return;
    }

    if (resource == null) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name());
    boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified());

    //  Gibt Fehler im Errorhandling von elect...
//    if (notModified) {
//      System.out.println("G");
//      response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
//      return;
//    }

    setContentHeaders(response, fileName, resource.getContentLength());

    if (head) {
      return;
    }

    writeContent(response, resource);

  }

  /**
   * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when
   * the resource does actually not exist. The servlet will then return a HTTP 404 error.
   *
   * @param request The involved HTTP servlet request.
   * @return The static resource associated with the given HTTP servlet request.
   * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid
   *                                  static resource request. The servlet will then return a HTTP 400 error.
   */
  @Nullable
  protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException;

  private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) {
    String eTag = String.format(ETAG_HEADER, fileName, lastModified);
    response.setHeader("ETag", eTag);
    response.setDateHeader("Last-Modified", lastModified);
    if (fileName.toLowerCase().endsWith("html") || fileName.toLowerCase().endsWith("htm")) {
      response.setDateHeader("Expires", System.currentTimeMillis() + HTML_EXPIRE_TIME_IN_MILLIS);
    } else {
      response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS);
    }
    return notModified(request, eTag, lastModified);
  }

  private boolean notModified(HttpServletRequest request, String eTag, long lastModified) {
    String ifNoneMatch = request.getHeader("If-None-Match");

    if (ifNoneMatch != null) {
      String[] matches = COMPILE.split(ifNoneMatch);
      Arrays.sort(matches);
      return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1);
    } else {
      long ifModifiedSince = request.getDateHeader("If-Modified-Since");
      return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis.
    }
  }

  private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) {
    response.setHeader("Content-Type", getServletContext().getMimeType(fileName));
    response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName));

    if (contentLength != -1) {
      response.setHeader("Content-Length", String.valueOf(contentLength));
    }
  }

  private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException {
    try (
        ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream());
        WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
    ) {
      ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
      long size = 0;

      while (inputChannel.read(buffer) != -1) {
        buffer.flip();
        size += outputChannel.write(buffer);
        buffer.clear();
      }

      if (resource.getContentLength() == -1 && !response.isCommitted()) {
        response.setHeader("Content-Length", String.valueOf(size));
      }
    }
  }

}
