/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.server.handlers.resource;

import io.undertow.UndertowLogger;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
import io.undertow.util.ETagUtils;
import io.undertow.util.FlexBase64;
import io.undertow.util.Headers;
import io.undertow.util.Methods;
import io.undertow.util.RedirectBuilder;
import io.undertow.util.StatusCodes;
import org.xnio.channels.Channels;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * @author Stuart Douglas
 */
public class DirectoryUtils {

    /**
     * Serve static resource for the directory listing
     *
     * @param exchange The exchange
     * @return true if resources were served
     */
    public static boolean sendRequestedBlobs(HttpServerExchange exchange) {
        ByteBuffer buffer = null;
        String type = null;
        String etag = null;
        String quotedEtag = null;
        if ("css".equals(exchange.getQueryString())) {
            buffer = Blobs.FILE_CSS_BUFFER.duplicate();
            type = "text/css";
            etag = Blobs.FILE_CSS_ETAG;
            quotedEtag = Blobs.FILE_CSS_ETAG_QUOTED;
        } else if ("js".equals(exchange.getQueryString())) {
            buffer = Blobs.FILE_JS_BUFFER.duplicate();
            type = "application/javascript";
            etag = Blobs.FILE_JS_ETAG;
            quotedEtag = Blobs.FILE_JS_ETAG_QUOTED;
        }

        if (buffer != null) {

            if(!ETagUtils.handleIfNoneMatch(exchange, new ETag(false, etag), false)) {
                exchange.setStatusCode(StatusCodes.NOT_MODIFIED);
                return true;
            }

            exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, String.valueOf(buffer.limit()));
            exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, type);
            exchange.getResponseHeaders().put(Headers.ETAG, quotedEtag);
            if (Methods.HEAD.equals(exchange.getRequestMethod())) {
                exchange.endExchange();
                return true;
            }
            exchange.getResponseSender().send(buffer);

            return true;
        }

        return false;
    }

    public static StringBuilder renderDirectoryListing(String path, Resource resource) {
        if (!path.endsWith("/")){
            path += "/";
        }
        StringBuilder builder = new StringBuilder();
        builder.append("<html>\n<head>\n<script src='").append(path).append("?js'></script>\n")
                .append("<link rel='stylesheet' type='text/css' href='").append(path).append("?css' />\n</head>\n");
        builder.append("<body onresize='growit()' onload='growit()'>\n<table id='thetable'>\n<thead>\n");
        builder.append("<tr><th class='loc' colspan='3'>Directory Listing - ").append(path).append("</th></tr>\n")
                .append("<tr><th class='label offset'>Name</th><th class='label'>Last Modified</th><th class='label'>Size</th></tr>\n</thead>\n")
                .append("<tfoot>\n<tr><th class=\"loc footer\" colspan=\"3\">Powered by Undertow</th></tr>\n</tfoot>\n<tbody>\n");

        int state = 0;
        String parent = null;
        if(path.length() > 1) {
            for (int i = path.length() - 1; i >= 0; i--) {
                if (state == 1) {
                    if (path.charAt(i) == '/') {
                        state = 2;
                    }
                } else if (path.charAt(i) != '/') {
                    if (state == 2) {
                        parent = path.substring(0, i + 1);
                        break;
                    }
                    state = 1;
                }
            }
            if(parent == null) {
                parent = "/";
            }
        }


        SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss", Locale.US);
        int i = 0;
        if (parent != null) {
            i++;
            builder.append("<tr class='odd'><td><a class='icon up' href='").append(parent).append("'>[..]</a></td><td>");
            builder.append(format.format((resource.getLastModified() == null ? new Date(0L) : resource.getLastModified())))
                    .append("</td><td>--</td></tr>\n");
        }

        for (Resource entry : resource.list()) {
            builder.append("<tr class='").append((++i & 1) == 1 ? "odd" : "even").append("'><td><a class='icon ");
            builder.append(entry.isDirectory() ? "dir" : "file");
            builder.append("' href='").append(path).append(entry.getName()).append("'>").append(entry.getName()).append("</a></td><td>");
            builder.append(format.format((entry.getLastModified() == null) ? new Date(0L) : entry.getLastModified()))
                    .append("</td><td>");
            if (entry.isDirectory()) {
                builder.append("--");
            } else {
                formatSize(builder, entry.getContentLength());
            }
            builder.append("</td></tr>\n");
        }
        builder.append("</tbody>\n</table>\n</body>\n</html>");

        return builder;

    }

    public static void renderDirectoryListing(HttpServerExchange exchange, Resource resource) {
        String requestPath = exchange.getRequestPath();
        if (! requestPath.endsWith("/")) {
            exchange.setStatusCode(StatusCodes.FOUND);
            exchange.getResponseHeaders().put(Headers.LOCATION, RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true));
            exchange.endExchange();
            return;
        }

        StringBuilder builder = renderDirectoryListing(requestPath, resource);

        try {
            ByteBuffer output = ByteBuffer.wrap(builder.toString().getBytes(StandardCharsets.UTF_8));
            exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html; charset=UTF-8");
            exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, String.valueOf(output.limit()));
            exchange.getResponseHeaders().put(Headers.LAST_MODIFIED, DateUtils.toDateString(new Date()));
            exchange.getResponseHeaders().put(Headers.CACHE_CONTROL, "must-revalidate");
            Channels.writeBlocking(exchange.getResponseChannel(), output);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        } catch (IOException e) {
            UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
            exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
        }

        exchange.endExchange();
    }


    private static StringBuilder formatSize(StringBuilder builder, Long size) {
        if(size == null) {
            builder.append("???");
            return builder;
        }
        int n = 1024 * 1024 * 1024;
        int type = 0;
        while (size < n && n >= 1024) {
            n /= 1024;
            type++;
        }

        long top = (size * 100) / n;
        long bottom =  top % 100;
        top /= 100;

        builder.append(top);
        if (bottom > 0) {
            builder.append(".").append(bottom / 10);
            bottom %= 10;
            if (bottom > 0) {
                builder.append(bottom);
            }

        }

        switch (type) {
            case 0: builder.append(" GB"); break;
            case 1: builder.append(" MB"); break;
            case 2: builder.append(" KB"); break;
        }

        return builder;
    }



    private DirectoryUtils() {

    }

    /**
     * Constant Content
     *
     * @author Jason T. Greene
     */
    public static class Blobs {
          public static final String FILE_JS="function growit() {\n" +
                  "    var table = document.getElementById(\"thetable\");\n" +
                  "\n" +
                  "    var i = table.rows.length - 1;\n" +
                  "    while (i-- > 0) {\n" +
                  "        if (table.rows[i].id == \"eraseme\") {\n" +
                  "            table.deleteRow(i);\n" +
                  "        } else {\n" +
                  "            break;\n" +
                  "        }\n" +
                  "    }\n" +
                  "    table.style.height=\"\";\n" +
                  "    var i = 0;\n" +
                  "    while (table.offsetHeight < window.innerHeight - 24) {\n" +
                  "        i++;\n" +
                  "        var tbody = table.tBodies[0];\n" +
                  "        var row = tbody.insertRow(tbody.rows.length);\n" +
                  "        row.id=\"eraseme\";\n" +
                  "        var cell = row.insertCell(0);\n" +
                  "        if (table.rows.length % 2 != 0) {\n" +
                  "            row.className=\"even eveninvis\";\n" +
                  "        } else {\n" +
                  "            row.className=\"odd oddinvis\";\n" +
                  "        }\n" +
                  "\n" +
                  "        cell.colSpan=3;\n" +
                  "        cell.appendChild(document.createTextNode(\"i\"));\n" +
                  "    }\n" +
                  "    table.style.height=\"100%\";\n" +
                  "    if (i > 0) {\n" +
                  "        document.documentElement.style.overflowY=\"hidden\";\n" +
                  "    } else {\n" +
                  "        document.documentElement.style.overflowY=\"auto\";\n" +
                  "    }\n" +
                  "}";
          public static final String FILE_JS_ETAG = md5(FILE_JS.getBytes(StandardCharsets.US_ASCII));
          public static final String FILE_JS_ETAG_QUOTED = '"' + FILE_JS_ETAG + '"';
          public static final String FILE_CSS =
                  "body {\n" +
                  "    font-family: \"Lucida Grande\", \"Lucida Sans Unicode\", \"Trebuchet MS\", Helvetica, Arial, Verdana, sans-serif;\n" +
                  "    margin: 5px;\n" +
                  "}\n" +
                  "\n" +
                  "th.loc {\n" +
                  "    background-image: linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" +
                  "    background-image: -o-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" +
                  "    background-image: -moz-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" +
                  "    background-image: -webkit-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" +
                  "    background-image: -ms-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" +
                  "    \n" +
                  "    background-image: -webkit-gradient(\n" +
                  "        linear,\n" +
                  "        left bottom,\n" +
                  "        left top,\n" +
                  "        color-stop(0.08, rgb(153,151,153)),\n" +
                  "        color-stop(0.54, rgb(199,199,199))\n" +
                  "    );\n" +
                  "    color: black;\n" +
                  "    padding: 2px;\n" +
                  "    font-weight: normal;\n" +
                  "    border: solid 1px;\n" +
                  "    font-size: 150%;\n" +
                  "    text-align: left;\n" +
                  "}\n" +
                  "\n" +
                  "th.label {\n" +
                  "    border: solid  1px;\n" +
                  "    text-align: left;\n" +
                  "    padding: 4px;\n" +
                  "    padding-left: 8px;\n" +
                  "    font-weight: normal;\n" +
                  "    font-size: small;\n" +
                  "    background-color: #e8e8e8;\n" +
                  "}\n" +
                  "\n" +
                  "th.offset {\n" +
                  "    padding-left: 32px;\n" +
                  "}\n" +
                  "\n" +
                  "th.footer {\n" +
                  "    font-size: 75%;\n" +
                  "    text-align: right;\n" +
                  "}\n" +
                  "\n" +
                  "a.icon {\n" +
                  "    padding-left: 24px;\n" +
                  "    text-decoration: none;\n" +
                  "    color: black;\n" +
                  "}\n" +
                  "\n" +
                  "a.icon:hover {\n" +
                  "    text-decoration: underline;\n" +
                  "}\n" +
                  "\n" +
                  "table {\n" +
                  "    border: 1px solid;\n" +
                  "    border-spacing: 0px;\n" +
                  "    width: 100%;\n" +
                  "    border-collapse: collapse;\n" +
                  "}\n" +
                  "\n" +
                  "tr.odd {\n" +
                  "    background-color: #f3f6fa;\n" +
                  "}\n" +
                  "\n" +
                  "tr.odd td {\n" +
                  "    padding: 2px;\n" +
                  "    padding-left: 8px;\n" +
                  "    font-size: smaller;\n" +
                  "}\n" +
                  "\n" +
                  "tr.even {\n" +
                  "    background-color: #ffffff;\n" +
                  "}\n" +
                  "\n" +
                  "tr.even td {\n" +
                  "    padding: 2px;\n" +
                  "    padding-left: 8px;\n" +
                  "    font-size: smaller;\n" +
                  "}\n" +
                  "\n" +
                  "tr.eveninvis td {\n" +
                  "    color: #ffffff;\n" +
                  "}\n" +
                  "\n" +
                  "tr.oddinvis td {\n" +
                  "    color: #f3f6fa\n" +
                  "}\n" +
                  "\n" +
                  "a.up {\n" +
                  "    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABI0lEQVQ4y2P4//8/Ay7sM4nhPwjjUwMm0ua//Y+M0+e//QrSGDAfgvEZAjdgydHXcAzTXLjWDoxhhqBbhGLA1N0vwBhdM7ohMHVwA8yrzn4zLj/936j8FE7N6IaA1IL0gPQy2DVc+rnp3FeCmtENAekB6WXw7Lz1tWD5x/+wEIdhdI3o8iA9IL0MYZMfvq9a9+V/w+avcIzLAGQ1ID0gvQxJc56/aNn29X/vnm9wjMsAZDWtQD0gvQwFy94+6N37/f/Moz/gGJcByGpAekB6GarXf7427ciP/0vP/YRjdP/CMLIakB6QXobKDd9PN+769b91P2kYpAekl2HJhb8r11/583/9ZRIxUM+8U783MQCBGBDXAHEbibgGrBdfTiMGU2wAAPz+nxp+TnhDAAAAAElFTkSuQmCC') left center no-repeat; background-size: 16px 16px;\n" +
                  "}\n" +
                  "\n" +
                  "a.dir {\n" +
                  "    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXZwQWcAAAAQAAAAEABcxq3DAAAA+UlEQVQ4jWP4//8/AyUYTKTNf/sfGafPf/s1be47G5IMWHL0NRxP2f3mbcaCtz/RDUbHKAZM3f2CJAw3wLzq7Dfj8tP/jcpPkYRBekB6GewaLv3cdO7r/y0XSMMgPSC9DJ6dt74WLP/4v3TVZ5IwSA9IL0PY5Ifvq9Z9+d+w+StJGKQHpJchac7zFy3bvv7v3fONJNwK1APSy5C/7O2D3r3f/888+oMkDNID0stQvf7ztWlHfvxfeu4nSRikB6SXoXLD99ONu379b91PGgbpAellWHLh38r1V/78X3+ZRAzUM/fUr00MQCAGxDVA3EYirgHrpUpupAQDAPs+7c1tGDnPAAAAAElFTkSuQmCC') left center no-repeat; background-size: 16px 16px;\n" +
                  "}\n" +
                  "\n" +
                  "a.file {\n" +
                  "    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXZwQWcAAAAQAAAAEABcxq3DAAABM0lEQVQ4y5WSTW6DMBCF3xvzc4wuOEIO0kVAuUB7vJ4g3KBdoHSRROomEpusUaoAcaYLfmKoqVRLIxnJ7/M3YwJVBcknACv8b+1U9SvoP1bXa/3WNDVIAQmQBLsNOEsGQYAwDNcARgDqusbl+wIRA2NkBEyqP0s+kCOAQhhjICJdkaDIJDwEvQAhH+G+SHagWTsi4jHoAWYIOxYDZDjnb8Fn4Akvz6AHcAbx3Tp5ETwI3RwckyVtv4Fr4VEe9qq6bDB5tlnYWou2bWGtRRRF6jdwAm5Za1FVFc7nM0QERVG8A9hPDRaGpapomgZlWSJJEuR5ftpsNq8ADr9amC+SuN/vuN1uIIntdnvKsuwZwKf2wxgBxpjpX+dA4jjW4/H4kabpixt2AbvAmDX+XnsAB509ww+A8mAar+XXgQAAAABJRU5ErkJggg==') left center no-repeat;\n" +
                  "}";
        public static final String FILE_CSS_ETAG = md5(FILE_CSS.getBytes(StandardCharsets.US_ASCII));
        public static final String FILE_CSS_ETAG_QUOTED = '"' + FILE_CSS_ETAG + '"';


        public static final ByteBuffer FILE_CSS_BUFFER;
        public static final ByteBuffer FILE_JS_BUFFER;

        static {
            try {
                byte[] bytes = FILE_CSS.getBytes(StandardCharsets.US_ASCII);
                FILE_CSS_BUFFER = ByteBuffer.allocateDirect(bytes.length);
                FILE_CSS_BUFFER.put(bytes);
                FILE_CSS_BUFFER.flip();

                bytes = FILE_JS.getBytes(StandardCharsets.US_ASCII);
                FILE_JS_BUFFER = ByteBuffer.allocateDirect(bytes.length);
                FILE_JS_BUFFER.put(bytes);
                FILE_JS_BUFFER.flip();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

    }


    /**
     * Generate the MD5 hash out of the given {@link ByteBuffer}
     */
    private static String md5(byte[] buffer) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(buffer);
            byte[] digest = md.digest();
            return new String(FlexBase64.encodeBytes(digest, 0, digest.length, false), StandardCharsets.US_ASCII);
        } catch (NoSuchAlgorithmException e) {
            // Should never happen
            throw new InternalError("MD5 not supported on this platform");
        }
    }

}
