/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.redkale.net.http;

import java.io.*;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.logging.*;
import java.util.regex.*;
import org.redkale.util.*;

/**
 * 静态资源HttpServlet
 *
 * <p>
 * 详情见: https://redkale.org
 *
 * @author zhangjx
 */
public class HttpResourceServlet extends HttpServlet {

    protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName());

    protected class WatchThread extends Thread {

        protected final File root;

        protected final WatchService watcher;

        public WatchThread(File root) throws IOException {
            this.root = root;
            this.setName("Redkale-HttpResourceServlet-Watch-Thread");
            this.setDaemon(true);
            this.watcher = this.root.toPath().getFileSystem().newWatchService();
        }

        @Override
        public void run() {
            try {
                final String rootstr = root.getCanonicalPath();
                while (!this.isInterrupted()) {
                    final WatchKey key = watcher.take();
                    final Path parent = keymaps.get(key);
                    if (parent == null) {
                        key.cancel();
                        continue;
                    }
                    key.pollEvents().stream().forEach((event) -> {
                        try {
                            Path path = parent.resolve((Path) event.context());
                            final String uri = path.toString().substring(rootstr.length()).replace('\\', '/');
                            //logger.log(Level.FINEST, "file(" + uri + ") happen " + event.kind() + " event");
                            if (event.kind() == ENTRY_DELETE) {
                                FileEntry en = files.remove(uri);
                                if (en != null) {
                                    en.remove();
                                }
                            } else if (event.kind() == ENTRY_MODIFY) {
                                FileEntry en = files.get(uri);
                                if (en != null && en.file != null) {
                                    long d;  //等待update file完毕
                                    for (;;) {
                                        d = en.file.lastModified();
                                        Thread.sleep(2000L);
                                        if (d == en.file.lastModified()) {
                                            break;
                                        }
                                    }
                                    en.update();
                                }
                            }
                        } catch (Exception ex) {
                            logger.log(Level.FINE, event.context() + " occur erroneous", ex);
                        }
                    });
                    key.reset();
                }
            } catch (Exception e) {
                //do nothing
            }
        }
    }

    protected final LongAdder cachedLength = new LongAdder();

    //缓存总大小, 默认0
    protected long cachelimit = 0 * 1024 * 1024L;

    //最大可缓存的文件大小，  大于该值的文件将不被缓存
    protected long cachelengthmax = 1 * 1024 * 1024;

    //是否监控缓存文件的变化， 默认不监控
    protected boolean watch = false;

    protected File root = new File("./root/");

    protected String indexHtml = "index.html";

    protected final ConcurrentHashMap<String, FileEntry> files = new ConcurrentHashMap<>();

    protected final ConcurrentHashMap<WatchKey, Path> keymaps = new ConcurrentHashMap<>();

    protected SimpleEntry<Pattern, String>[] locationRewrites;

    protected WatchThread watchThread;

    protected String[] renderSuffixs;

    @Override
    public void init(HttpContext context, AnyValue config) {
        if (config != null) {
            String rootstr = config.getValue("webroot", "root");
            this.indexHtml = config.getValue("index", "index.html");
            if (rootstr.indexOf(':') < 0 && rootstr.indexOf('/') != 0 && System.getProperty("APP_HOME") != null) {
                rootstr = new File(System.getProperty("APP_HOME"), rootstr).getPath();
            }
            try {
                this.root = new File(rootstr).getCanonicalFile();
            } catch (IOException ioe) {
                this.root = new File(rootstr);
            }
            AnyValue cacheconf = config.getAnyValue("cache");
            if (cacheconf != null) {
                this.cachelimit = parseLenth(cacheconf.getValue("limit"), 0 * 1024 * 1024L);
                this.cachelengthmax = parseLenth(cacheconf.getValue("lengthmax"), 1 * 1024 * 1024L);
                this.watch = cacheconf.getBoolValue("watch", false);
            }
            List<SimpleEntry<Pattern, String>> locations = new ArrayList<>();
            for (AnyValue av : config.getAnyValues("rewrite")) {
                if ("location".equals(av.getValue("type"))) {
                    String m = av.getValue("match");
                    String f = av.getValue("forward");
                    if (m != null && f != null) {
                        locations.add(new SimpleEntry<>(Pattern.compile(m), f));
                    }
                }
            }
            this.locationRewrites = locations.isEmpty() ? null : locations.toArray(new SimpleEntry[locations.size()]);
        }
        if (this.cachelimit < 1) {
            return;  //不缓存不需要开启WatchThread监听
        }
        if (this.root != null && this.watch) {
            try {
                this.watchThread = new WatchThread(this.root);
                this.watchThread.start();
            } catch (IOException ex) {
                logger.log(Level.WARNING, HttpResourceServlet.class.getSimpleName() + " start watch-thread error", ex);
            }
        }
    }

    @Override
    public void destroy(HttpContext context, AnyValue config) {
        if (this.watchThread != null) {
            try {
                this.watchThread.watcher.close();
            } catch (IOException ex) {
                logger.log(Level.WARNING, HttpResourceServlet.class.getSimpleName() + " close watch-thread error", ex);
            }
            if (this.watchThread.isAlive()) {
                this.watchThread.interrupt();
            }
        }
    }

    public void setRoot(String rootstr) {
        if (rootstr == null) {
            return;
        }
        try {
            this.root = new File(rootstr).getCanonicalFile();
        } catch (IOException ioe) {
            this.root = new File(rootstr);
        }
    }

    public void setRoot(File file) {
        if (file == null) {
            return;
        }
        try {
            this.root = file.getCanonicalFile();
        } catch (IOException ioe) {
            this.root = file;
        }
    }

    protected static long parseLenth(String value, long defValue) {
        if (value == null) {
            return defValue;
        }
        value = value.toUpperCase().replace("B", "");
        if (value.endsWith("G")) {
            return Long.decode(value.replace("G", "")) * 1024 * 1024 * 1024;
        }
        if (value.endsWith("M")) {
            return Long.decode(value.replace("M", "")) * 1024 * 1024;
        }
        if (value.endsWith("K")) {
            return Long.decode(value.replace("K", "")) * 1024;
        }
        return Long.decode(value);
    }

    @Override
    public void execute(HttpRequest request, HttpResponse response) throws IOException {
        String uri = request.getRequestPath();
        if (uri.contains("../")) {
            if (logger.isLoggable(Level.FINEST)) {
                logger.log(Level.FINEST, "Not found resource (404) be " + uri + ", request = " + request);
            }
            finish404(request, response);
            return;
        }
        if (locationRewrites != null) {
            for (SimpleEntry<Pattern, String> entry : locationRewrites) {
                Matcher matcher = entry.getKey().matcher(uri);
                if (matcher.find()) {
                    StringBuffer sb = new StringBuffer(uri.length());
                    matcher.appendReplacement(sb, entry.getValue());
                    matcher.appendTail(sb);
                    uri = sb.toString();
                    break;
                }
            }
        }
        if (uri.length() == 0 || uri.equals("/")) {
            uri = this.indexHtml.indexOf('/') == 0 ? this.indexHtml : ("/" + this.indexHtml);
        }
        //跳过模板引擎的后缀文件
        if (renderSuffixs != null) {
            String suri = uri.toLowerCase();
            for (String suffix : renderSuffixs) {
                if (suri.endsWith(suffix)) {
                    finish404(request, response);
                    return;
                }
            }
        }
        //System.out.println(request);
        FileEntry entry;
        if (watchThread == null && files.isEmpty()) {
            entry = createFileEntry(uri);
        } else {  //有缓存
            entry = files.computeIfAbsent(uri, this::createFileEntry);
        }
        if (entry == null) {
            if (logger.isLoggable(Level.FINER)) {
                logger.log(Level.FINER, "Not found resource (404), url = " + request.getRequestPath());
            }
            finish404(request, response);
        } else {
            //file = null 表示资源内容在内存而不是在File中
            //file = null 时必须传 fileName
            response.finishFile(entry.file == null ? entry.fileName : null, entry.file, entry.content);
        }
    }

    protected FileEntry createFileEntry(String uri) {
        File rfile = new File(root, uri);
        File file = rfile;
        if (file.isDirectory()) {
            file = new File(rfile, this.indexHtml);
        }
        if (file.isDirectory()) {
            file = new File(rfile, "index.html");
        }
        if (!file.isFile() || !file.canRead()) {
            return null;
        }
        FileEntry en = new FileEntry(this, file);
        if (watchThread == null) {
            return en;
        }
        try {
            Path p = file.getParentFile().toPath();
            keymaps.put(p.register(watchThread.watcher, ENTRY_MODIFY, ENTRY_DELETE), p);
        } catch (IOException e) {
            logger.log(Level.INFO, HttpResourceServlet.class.getSimpleName() + " watch FileEntry(" + uri + ") erroneous", e);
        }
        return en;
    }

    protected static class FileEntry {

        protected final String fileName;

        protected final File file; //如果所有资源文件打包成zip文件则file=null

        protected final HttpResourceServlet servlet;

        protected ByteArray content;

        @SuppressWarnings("OverridableMethodCallInConstructor")
        public FileEntry(final HttpResourceServlet servlet, File file) {
            this.servlet = servlet;
            this.file = file;
            this.fileName = file.getName();
            update();
        }

        public FileEntry(final HttpResourceServlet servlet, String fileName, ByteArray content) {
            this.servlet = servlet;
            this.file = null;
            this.fileName = fileName;
            this.content = content;
            this.servlet.cachedLength.add(this.content.length());
        }

        public FileEntry(final HttpResourceServlet servlet, String filename, InputStream in) throws IOException {
            ByteArray out = new ByteArray();
            byte[] bytes = new byte[10240];
            int pos;
            while ((pos = in.read(bytes)) != -1) {
                out.put(bytes, 0, pos);
            }
            this.servlet = servlet;
            this.file = null;
            this.fileName = filename;
            this.content = out;
            this.servlet.cachedLength.add(this.content.length());
        }

        public void update() {
            if (this.file == null) {
                return;
            }
            if (this.content != null) {
                this.servlet.cachedLength.add(0L - this.content.length());
                this.content = null;
            }
            long length = this.file.length();
            if (length > this.servlet.cachelengthmax) {
                return;
            }
            if (this.servlet.cachedLength.longValue() + length > this.servlet.cachelimit) {
                return; //超过缓存总容量
            }
            try (FileInputStream in = new FileInputStream(file)) {
                ByteArray out = new ByteArray((int) file.length());
                byte[] bytes = new byte[10240];
                int pos;
                while ((pos = in.read(bytes)) != -1) {
                    out.put(bytes, 0, pos);
                }
                this.content = out;
                this.servlet.cachedLength.add(this.content.length());
            } catch (Exception e) {
                this.servlet.logger.log(Level.INFO, HttpResourceServlet.class.getSimpleName() + " update FileEntry(" + file + ") erroneous", e);
            }
        }

        public void remove() {
            if (this.content != null) {
                this.servlet.cachedLength.add(0L - this.content.length());
            }
        }

        public long getCachedLength() {
            return this.content == null ? 0L : this.content.length();
        }

    }
}
