/**
 * Copyright (C) 2009-2013 FoundationDB, LLC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.foundationdb.rest;

import com.foundationdb.rest.resources.ResourceHelper;
import com.foundationdb.server.Quote;
import com.foundationdb.server.error.ErrorCode;
import com.foundationdb.server.error.NoSuchRoutineException;
import com.foundationdb.server.error.NoSuchTableException;
import com.foundationdb.sql.embedded.JDBCException;
import com.foundationdb.util.AkibanAppender;
import com.fasterxml.jackson.core.JsonParseException;

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

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

public class RestResponseBuilder {
    public interface BodyGenerator {
        public void write(PrintWriter writer) throws Exception;
    }

    private static final Charset UTF8 = Charset.forName("UTF-8");
    private static final Map<Class,Response.Status> EXCEPTION_STATUS_MAP = buildExceptionStatusMap();
    private static final Logger LOG = LoggerFactory.getLogger(RestResponseBuilder.class);

    private final HttpServletRequest request;
    private final boolean isJsonp;
    private BodyGenerator outputGenerator;
    private String outputBody;
    private String jsonp;
    private int status;
    private MediaType type;


    public RestResponseBuilder(HttpServletRequest request, String jsonp) {
        this.request = request;
        this.jsonp = jsonp;
        this.isJsonp = jsonp != null;
        this.status = Response.Status.OK.getStatusCode();
    }

    public static RestResponseBuilder forRequest(HttpServletRequest request) {
        return new RestResponseBuilder(request, request.getParameter(ResourceHelper.JSONP_ARG_NAME));
    }

    public RestResponseBuilder status(Response.Status status) {
        this.status = status.getStatusCode();
        return this;
    }
    
    public RestResponseBuilder type(MediaType type) {
        this.type = type;
        return this;
    }

    public RestResponseBuilder body(String outputBody) {
        this.outputBody = outputBody;
        this.outputGenerator = null;
        return this;
    }

    public RestResponseBuilder body(ErrorCode code, String message) {
        body(formatErrorWithJsonp(code.getFormattedValue(), message));
        return this;
    }

    public RestResponseBuilder body(BodyGenerator outputGenerator) {
        this.outputBody = null;
        this.outputGenerator = outputGenerator;
        return this;
    }

    public Response build() {
        if(outputBody == null && outputGenerator == null && jsonp == null) {
            status(Response.Status.NO_CONTENT);
        }
        if (isJsonp) {
            status(Response.Status.OK);
        }
        Response.ResponseBuilder builder;
        if (this.status == Response.Status.NO_CONTENT.getStatusCode()) {
            builder = Response.status(status).type((MediaType)null);
        } else {
            builder = Response.status(status).entity(createStreamingOutput());
        }
        if(isJsonp) {
            builder.type(ResourceHelper.APPLICATION_JAVASCRIPT_TYPE);
        } else if (type != null) {
            builder.type(type);
        }
        return builder.build();
    }
    
    public static void formatJsonError(StringBuilder builder, String code, String message, String note) {
        builder.append("{\"code\":\"");
        builder.append(code);
        builder.append("\", \"message\":\"");
        Quote.JSON_QUOTE.append(AkibanAppender.of(builder), message);
        if (note != null) {
            builder.append("\", \"note\":\"");
            Quote.JSON_QUOTE.append(AkibanAppender.of(builder), note);
        }
        builder.append("\"}");
    }


    private String formatErrorWithJsonp(String code, String message) {
        StringBuilder builder = new StringBuilder();
        if(isJsonp) {
            builder.append(jsonp);
            builder.append('(');
        }
        formatJsonError(builder, code, message, null);
        if(isJsonp) {
            builder.append(')');
        }
        builder.append('\n');
        return builder.toString();
    }

    public WebApplicationException wrapException(Throwable e) {
        final ErrorCode code = ErrorCode.getCodeForRESTException(e);
        Response.Status status = EXCEPTION_STATUS_MAP.get(e.getClass());
        if (e instanceof JDBCException) {
            if (e.getCause() != null) {
                e = e.getCause();
            }
        }
        if(status == null) {
            status = Response.Status.CONFLICT;
        }
        code.logAtImportance(
                LOG,
                LOG.isDebugEnabled() ? "Exception from request(method: {}, url: {}, params: {})"
                                     : "Exception from request(method: {}, url: {}, params: {}): {}",
                request.getMethod(), request.getRequestURL(), request.getQueryString(),
                e
        );
        String exMsg = (e.getMessage() != null) ? e.getMessage() : e.getClass().getName();
        return new WebApplicationException(
                Response.status(status)
                        .entity(formatErrorWithJsonp(code.getFormattedValue(), exMsg))
                        .type(isJsonp ? ResourceHelper.APPLICATION_JAVASCRIPT_TYPE : MediaType.APPLICATION_JSON_TYPE)
                        .build()
        );
    }

    private StreamingOutput createStreamingOutput() {
        return new StreamingOutput() {
            @Override
            public void write(OutputStream output)  {
                try {
                    PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, UTF8), false);
                    if(isJsonp) {
                        writer.write(jsonp);
                        writer.write('(');
                    }
                    if(outputGenerator != null) {
                        outputGenerator.write(writer);
                    } else if(outputBody != null) {
                        writer.write(outputBody);
                    }
                    if(isJsonp) {
                        writer.write(')');
                    }
                    writer.write('\n');
                    writer.flush();
                    writer.close();
                } catch(Throwable t) {
                    throw wrapException(t);
                }
            }
        };
    }

    private static Map<Class, Response.Status> buildExceptionStatusMap() {
        Map<Class, Response.Status> map = new HashMap<>();
        map.put(NoSuchTableException.class, Response.Status.NOT_FOUND);
        map.put(NoSuchRoutineException.class, Response.Status.NOT_FOUND);
        map.put(JsonParseException.class, Response.Status.BAD_REQUEST);
        return map;
    }
}

@@ -5271,20 +5271,34 @@
         
-if (
+Throwable cause = 
 e instan
@@ -5319,28 +5319,11 @@
 tion
-) %7B%0A            if (
+ ? 
 e.ge
@@ -5335,76 +5335,15 @@
 e() 
-!=
+:
  null
-) %7B%0A                e = e.getCause();%0A            %7D%0A        %7D
+;
 %0A   
@@ -5779,16 +5779,40 @@
         
+cause == null ? e : caus
 e%0A      

change to logging