/**
 *  FileHandler
 *  Copyright 10.02.2016 by Michael Peter Christen, @0rb1t3r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *  
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package net.yacy.grid.http;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;

import net.yacy.grid.mcp.Data;
import net.yacy.grid.tools.ByteBuffer;


public class FileHandler extends ResourceHandler implements Handler {
    
    private final long CACHE_LIMIT = 128L * 1024L;
    
    /**
     * create a custom ResourceHandler with more caching
     * @param expiresSeconds the time each file shall stay in the cache
     */
    public FileHandler() {
        //this.setMinMemoryMappedContentLength((int) CACHE_LIMIT);
    }
    
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // use the ResourceHandler to handle the request. This method calls doResponseHeaders internally
        super.handle(target, baseRequest, request, response);
    }

    /*
    @Override
    protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType) {
	if (mimeType == null && resource.getName().endsWith(".css")) mimeType = "text/css";
        super.doResponseHeaders(response, resource, mimeType);
        // modify the caching strategy of ResourceHandler
        setCaching(response, this.expiresSeconds);
    }
    */
    
    public static void setCaching(final HttpServletResponse response, final int expiresSeconds) {
        if (response instanceof org.eclipse.jetty.server.Response) {
            org.eclipse.jetty.server.Response r = (org.eclipse.jetty.server.Response) response;
            HttpFields fields = r.getHttpFields();
            
            // remove the last-modified field since caching otherwise does not work
            /*
               https://www.ietf.org/rfc/rfc2616.txt
               "if the response does have a Last-Modified time, the heuristic
               expiration value SHOULD be no more than some fraction of the interval
               since that time. A typical setting of this fraction might be 10%."
            */
            fields.remove(HttpHeader.LAST_MODIFIED); // if this field is present, the reload-time is a 10% fraction of ttl and other caching headers do not work

            // cache-control: allow shared caching (i.e. proxies) and set expires age for cache
            if(expiresSeconds == 0){
                fields.put(HttpHeader.CACHE_CONTROL, "public, no-store, max-age=" + Integer.toString(expiresSeconds)); // seconds
            }
            else {
                fields.put(HttpHeader.CACHE_CONTROL, "public, max-age=" + Integer.toString(expiresSeconds)); // seconds
            }
        } else {
            response.setHeader(HttpHeader.LAST_MODIFIED.asString(), ""); // not really the best wqy to remove this header but there is no other option
            if(expiresSeconds == 0){
                response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "public, no-store, max-age=" + Integer.toString(expiresSeconds));
            }
            else{
                response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "public, max-age=" + Integer.toString(expiresSeconds));
            }

        }

        // expires: define how long the file shall stay in a cache if cache-control is not used for this information
        response.setDateHeader(HttpHeader.EXPIRES.asString(), System.currentTimeMillis() + expiresSeconds * 1000);
    }

    @Override
    public Resource getResource(String path) {
        try {
            Resource resource = super.getResource(path);
            if (resource == null) return null;
            if (!(resource instanceof PathResource) || !resource.exists()) return resource;
            File f = resource.getFile();
            if (f.isDirectory() && !path.equals("/")) return resource;
            CacheResource cache = resourceCache.get(f);
            if (cache != null) return cache;
            if (f.length() < CACHE_LIMIT || f.getName().endsWith(".html") || path.equals("/")) {
                cache = new CacheResource((PathResource) resource);
                resourceCache.put(f, cache);
                return cache;
            }
            return resource;
        } catch (IOException e) {
            Data.logger.warn("", e);
        }
        return null;
    }

    private final Map<File, CacheResource> resourceCache = new ConcurrentHashMap<>();
    private final static byte[] SSI_START = "<!--#include file=\"".getBytes();
    private final static byte[] SSI_END   = "\" -->".getBytes();
    
    private static class CacheResource extends Resource {

        private byte[] buffer;
        private long lastModified;
        private File file;
        private List<File> includes;
        
        public CacheResource(PathResource pathResource) throws IOException {
            this.file = pathResource.getFile();
            if (this.file.isDirectory()) this.file = new File(this.file, "index.html");
            this.includes = new ArrayList<>(8);
            initCache(System.currentTimeMillis());
            pathResource.close();
            
        }
        
        private void initCache(long nextLastModified) throws IOException {
            this.buffer = Files.readAllBytes(this.file.toPath());
            if (this.file.getName().endsWith(".html")) this.buffer = insertSSI(this.buffer);
            this.lastModified = nextLastModified;
        }
        
        private byte[] insertSSI(byte[] b) throws IOException {
            this.includes.clear();
            for (int p = findSSI_start(b, 0); p >= 0; p = findSSI_start(b, p)) {
                int q = findSSI_end(b, p);
                if (q < 0) break;
                byte[] f = new byte[q - p - SSI_START.length];
                System.arraycopy(b, p + SSI_START.length, f, 0, f.length);
                File ff = new File(this.file.getParent(), new String(f, StandardCharsets.UTF_8)).getCanonicalFile();
                if (!ff.exists()) {
                    byte[] b0 = new byte[b.length - (q - p) - SSI_END.length];
                    System.arraycopy(b, 0, b0, 0, p);
                    System.arraycopy(b, q + SSI_END.length, b0, p, b.length - q - SSI_END.length);
                    b = b0;
                    continue;
                }
                this.includes.add(ff);
                byte[] i = Files.readAllBytes(ff.toPath());
                byte[] b0 = new byte[b.length - (q - p) - SSI_END.length + i.length];
                System.arraycopy(b, 0, b0, 0, p);
                System.arraycopy(i, 0, b0, p, i.length);
                System.arraycopy(b, q + SSI_END.length, b0, p + i.length, b.length - q - SSI_END.length);
                b = b0;
            }
            return b;
        }

        private int findSSI_start(byte[] b, int p) {
            return ByteBuffer.indexOf(b, SSI_START, p);
        }
        
        private int findSSI_end(byte[] b, int p) {
            return ByteBuffer.indexOf(b, SSI_END, p);
        }

        @Override
        public boolean exists() {
            return true;
        }

        @Override
        public boolean isDirectory() {
            return false;
        }

        private long actualLastModified() {
            long l = this.file.lastModified();
            for (File d: this.includes) l = Math.max(l, d.lastModified());
            return l;
        }
        
        @Override
        public long lastModified() {
            long l = actualLastModified();
            if (actualLastModified() > this.lastModified) try {initCache(l);} catch (IOException e) {}
            this.lastModified = l;
            return this.lastModified;
        }

        @Override
        public long length() {
            return this.buffer.length;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            long l = actualLastModified();
            if (actualLastModified() > this.lastModified) initCache(l);
            return new ByteArrayInputStream(this.buffer);
        }

        @Override
        public ReadableByteChannel getReadableByteChannel() throws IOException {
            return Channels.newChannel(this.getInputStream());
        }

        @Override
        public String getName() {
            return this.file.getName();
        }

        @Override public void close() {}
        @Override public String[] list() {throw new UnsupportedOperationException();}
        @Override public boolean delete() throws SecurityException {throw new UnsupportedOperationException();}
        @Override public Resource addPath(String arg0) throws IOException, MalformedURLException {throw new UnsupportedOperationException();}
        @Override public File getFile() throws IOException {throw new UnsupportedOperationException();}
        @Override public URL getURL() {throw new UnsupportedOperationException();}
        @Override public boolean isContainedIn(Resource arg0) throws MalformedURLException {throw new UnsupportedOperationException();}
        @Override public boolean renameTo(Resource arg0) throws SecurityException {throw new UnsupportedOperationException();}
    }
    
    
}