package org.jboss.resteasy.jsapi;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.zip.GZIPOutputStream;

import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.jboss.resteasy.jsapi.i18n.LogMessages;
import org.jboss.resteasy.jsapi.i18n.Messages;
import org.jboss.resteasy.util.PathHelper;

/**
 * @author <a href="mailto:stef@epardaud.fr">Stéphane Épardaud</a>
 */
public class JSAPIWriter {

    private static final long serialVersionUID = -1985015444704126795L;

    public void writeJavaScript(String base, HttpServletRequest req, HttpServletResponse resp,
            Map<String, ServiceRegistry> serviceRegistries) throws IOException {
        LogMessages.LOGGER.debug(Messages.MESSAGES.startResteasyClient());

        // RESTEASY-776
        // before writing generated javascript, we generate Etag and compare it with client request.
        // If nothing changed, we send back 304 Not Modified for client browser to use cached js.
        String ifNoneMatch = req.getHeader("If-None-Match");
        String etag = generateEtag(serviceRegistries);
        resp.setHeader("Etag", etag);

        if (ifNoneMatch != null && ifNoneMatch.equals(etag)) {
            resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        for (Map.Entry<String, ServiceRegistry> entry : serviceRegistries.entrySet()) {
            String uri = base;
            if (entry.getKey() != null) {
                uri = resolveDoubleSlash(uri, entry.getKey(), true);
            }
            StringWriter stringWriter = new StringWriter();
            PrintWriter writer = new PrintWriter(new BufferedWriter(stringWriter));
            writeJavaScript(uri, writer, entry.getValue());
            writer.flush();
            writer.close();

            if (clientIsGzipSupported(req)) {
                ByteArrayOutputStream compressedContent = new ByteArrayOutputStream();
                GZIPOutputStream gzipstream = new GZIPOutputStream(compressedContent);
                gzipstream.write(stringWriter.toString().getBytes());
                gzipstream.finish();

                // get the compressed content
                byte[] compressedBytes = compressedContent.toByteArray();

                // set appropriate HTTP headers
                resp.setContentLength(compressedBytes.length);
                resp.addHeader("Content-Encoding", "gzip");

                ServletOutputStream output = resp.getOutputStream();
                output.write(compressedBytes);
                output.flush();
                output.close();

            } else {
                ServletOutputStream output = resp.getOutputStream();
                byte[] bytes = stringWriter.toString().getBytes();
                resp.setContentLength(bytes.length);
                output.write(bytes);
                output.flush();
                output.close();
            }
        }

    }

    private boolean clientIsGzipSupported(HttpServletRequest req) {
        String encoding = req.getHeader("Accept-Encoding");
        return encoding != null && encoding.contains("gzip");
    }

    public void writeJavaScript(String uri, PrintWriter writer,
            ServiceRegistry serviceRegistry) throws IOException {
        copyResource("/resteasy-client.js", writer);
        LogMessages.LOGGER.debug(Messages.MESSAGES.startJaxRsApi());
        LogMessages.LOGGER.debug(Messages.MESSAGES.restApiUrl(uri));
        writer.println("REST.apiURL = '" + uri + "';");
        Set<String> declaredPrefixes = new HashSet<String>();
        printService(writer, serviceRegistry, declaredPrefixes, uri);

    }

    private String generateEtag(Map<String, ServiceRegistry> serviceRegistries) {
        StringBuilder etagBuilder = new StringBuilder();
        for (Map.Entry<String, ServiceRegistry> entry : serviceRegistries.entrySet()) {
            if (entry.getKey() != null)
                etagBuilder.append(entry.getKey()).append(':');
            generateEtag(entry.getValue(), etagBuilder);
        }
        return String.valueOf(Math.abs(etagBuilder.toString().hashCode()));
    }

    private void generateEtag(ServiceRegistry serviceRegistry, StringBuilder etagBuilder) {
        for (MethodMetaData methodMetaData : serviceRegistry.getMethodMetaData()) {
            etagBuilder.append(methodMetaData.hashCode());

            for (ServiceRegistry subService : serviceRegistry.getLocators()) {
                generateEtag(subService, etagBuilder);
            }

        }
    }

    private void printService(PrintWriter writer,
            ServiceRegistry serviceRegistry, Set<String> declaredPrefixes, String uri) {

        for (MethodMetaData methodMetaData : serviceRegistry.getMethodMetaData()) {
            LogMessages.LOGGER.debug(Messages.MESSAGES.path(methodMetaData.getUri()));
            LogMessages.LOGGER.debug(Messages.MESSAGES.invoker(methodMetaData.getInvoker()));
            String declaringPrefix = methodMetaData.getFunctionPrefix(); // TODO Add prefix path segment
            declarePrefix(writer, declaringPrefix, declaredPrefixes);

            for (String httpMethod : methodMetaData.getHttpMethods()) {
                print(writer, httpMethod, methodMetaData, uri);
            }
        }
        for (ServiceRegistry subService : serviceRegistry.getLocators())
            printService(writer, subService, declaredPrefixes, uri);
    }

    private void declarePrefix(PrintWriter writer, String declaringPrefix, Set<String> declaredPrefixes) {
        if (declaredPrefixes.add(declaringPrefix)) {
            int lastDot = declaringPrefix.lastIndexOf(".");
            if (lastDot == -1)
                writer.println("var " + declaringPrefix + " = {};");
            else {
                declarePrefix(writer, declaringPrefix.substring(0, lastDot), declaredPrefixes);
                writer.println(declaringPrefix + " = {};");
            }
        }

    }

    private void copyResource(String name, PrintWriter writer)
            throws IOException {
        Reader reader = new InputStreamReader(getClass()
                .getResourceAsStream(name));
        char[] array = new char[1024];
        int read;
        while ((read = reader.read(array)) >= 0) {
            writer.write(array, 0, read);
        }
        reader.close();
    }

    private void print(PrintWriter writer, String httpMethod,
            MethodMetaData methodMetaData, String uri) {
        String methodUri = methodMetaData.getUri();
        writer.println("// " + httpMethod + " " + methodUri);
        writer
                .println(methodMetaData.getFunctionName() + " = function(_params){");
        writer.println(" var params = _params ? _params : {};");
        writer.println(" var request = new REST.Request();");
        writer.println(" request.setMethod('" + httpMethod + "');");
        writer
                .println(" var uri = params.$apiURL ? params.$apiURL : REST.apiURL;");
        if (methodUri.contains("{")) {
            printURIParams(methodUri, writer);
        } else {
            String resolvedMethodUri = resolveDoubleSlash(uri, methodUri, false);
            writer.println(" uri += '" + resolvedMethodUri + "';");
        }
        printOtherParams(methodMetaData, writer);
        writer.println(" request.setURI(uri);");
        writer.println(" if(params.$username && params.$password)");
        writer
                .println("  request.setCredentials(params.$username, params.$password);");
        writer.println(" if(params.$accepts)");
        writer.println("  request.setAccepts(params.$accepts);");
        if (methodMetaData.getWants() != null) {
            writer.println(" else");
            writer.println("  request.setAccepts('" + methodMetaData.getWants()
                    + "');");
        }

        writer.println("if (REST.antiBrowserCache == true) {");
        writer.println("  request.addQueryParameter('resteasy_jsapi_anti_cache', (new Date().getTime()));");
        writer.println("    var cached_obj = REST._get_cache_signature(REST._generate_cache_signature(uri));");

        writer.println(
                "    if (cached_obj != null) { request.addHeader('If-Modified-Since', cached_obj[1]['Last-Modified']); request.addHeader('If-None-Match', cached_obj[1]['Etag']);}");

        writer.println("}");

        writer.println(" if(params.$contentType)");
        writer.println("  request.setContentType(params.$contentType);");
        writer.println(" else");
        writer.println("  request.setContentType('"
                + methodMetaData.getConsumesMIMEType() + "');");
        writer.println(" if(params.$callback){");
        writer.println("  request.execute(params.$callback);");
        writer.println(" }else{");
        writer.println("  var returnValue;");
        writer.println("  request.setAsync(false);");
        writer
                .println("  var callback = function(httpCode, xmlHttpRequest, value){ returnValue = value;};");
        writer.println("  request.execute(callback);");
        writer.println("  return returnValue;");
        writer.println(" }");
        writer.println("};");
    }

    private void printOtherParams(MethodMetaData methodMetaData,
            PrintWriter writer) {
        List<MethodParamMetaData> params = methodMetaData.getParameters();
        for (MethodParamMetaData methodParamMetaData : params) {
            printParameter(methodParamMetaData, writer);
        }
    }

    private void printParameter(MethodParamMetaData metaData,
            PrintWriter writer) {
        switch (metaData.getParamType()) {
            case QUERY_PARAMETER:
                print(metaData, writer, "QueryParameter");
                break;
            case HEADER_PARAMETER:
                print(metaData, writer, "Header");
                // FIXME: warn about forbidden headers:
                // http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method
                break;
            case COOKIE_PARAMETER:
                print(metaData, writer, "Cookie");
                break;
            case MATRIX_PARAMETER:
                print(metaData, writer, "MatrixParameter");
                break;
            case FORM_PARAMETER:
                print(metaData, writer, "FormParameter");
                break;
            case FORM:
                print(metaData, writer, "Form");
                break;
            case ENTITY_PARAMETER:
                // the entity
                writer.println(" if(params.$entity)");
                writer.println("  request.setEntity(params.$entity);");
                break;
        }
    }

    private void print(MethodParamMetaData metaData, PrintWriter writer,
            String type) {
        String paramName = metaData.getParamName();
        writer.println(
                String.format(" if(Object.prototype.hasOwnProperty.call(params, '%s'))\n  request.add%s('%s', params.%s);",
                        paramName, type, paramName, paramName));
    }

    private void printURIParams(String uri, PrintWriter writer) {
        String replacedCurlyURI = PathHelper.replaceEnclosedCurlyBraces(uri);
        Matcher matcher = PathHelper.URI_PARAM_PATTERN.matcher(replacedCurlyURI);
        int i = 0;
        while (matcher.find()) {
            if (matcher.start() > i) {
                writer.println(" uri += '"
                        + replacedCurlyURI.substring(i, matcher.start()) + "';");
            }
            String name = matcher.group(1);
            writer.println(" uri += REST.Encoding.encodePathSegment(params." + name + ");");
            i = matcher.end();
        }
        if (i < replacedCurlyURI.length())
            writer.println(" uri += '" + replacedCurlyURI.substring(i) + "';");
    }

    /**
     * Resolves the case of {@code uri} ending with "/" and {@code uriToAppend} starting with "/" so that the combination
     * of both ends up with only one "/" instead of "//".
     *
     * <br>
     * <br>
     * Example:
     * http://localhost:8080/Uri//UriToAppend -> http://localhost:8080/Uri/UriToAppend
     *
     * @param uri         the base URI with the possible extra "/" at the end.
     * @param uriToAppend the URI being appended to the base one, with possible extra "/" at the beginning.
     * @param appendToUri true if the {@code uriToAppend} should be appended to the {@code uri}, false if the
     *                    {@code uriToAppend} should have the "/" removed without appending it to the {@code uri}.
     * @return The {@code uriToAppend} appended to the {@code uri} if the {@code appendToUri} is true, the {@code uriToAppend}
     *         without the extra "/" at the beginning if the {@code appendToUri} is false. In both cases there are no two "//"
     *         between the {@code uri} and the {@code uriToAppend}.
     */
    private String resolveDoubleSlash(String uri, String uriToAppend, boolean appendToUri) {
        if (uri.endsWith("/") && uriToAppend.startsWith("/")) {
            // Remove the extra "/" at the beginning of uriToAppend
            if (uriToAppend.length() > 1) {
                if (appendToUri) {
                    return uri.concat(uriToAppend.substring(1));
                } else {
                    return uriToAppend.substring(1);
                }
            }
            // Edge case if the uriToAppend is just "/"
            else {
                if (appendToUri) {
                    return uri;
                }
                return "";
            }
        }

        // If the
        if (appendToUri) {
            return uri.concat(uriToAppend);
        }
        return uriToAppend;
    }
}
