/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.component.sparkrest;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Iterator;
import java.util.Map;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.TypeConverter;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.MessageHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;

public class DefaultSparkBinding implements SparkBinding {

    private static final Logger LOG = LoggerFactory.getLogger(DefaultSparkBinding.class);
    private HeaderFilterStrategy headerFilterStrategy = new SparkHeaderFilterStrategy();

    @Override
    public Message toCamelMessage(Request request, Exchange exchange, SparkConfiguration configuration) throws Exception {
        LOG.trace("toCamelMessage: {}", request);

        SparkMessage answer = new SparkMessage(request, null);
        answer.setExchange(exchange);
        if (configuration.isMapHeaders()) {
            populateCamelHeaders(request, answer.getHeaders(), exchange, configuration);
        }

        if (configuration.isDisableStreamCache()) {
            // keep the body as a input stream
            answer.setBody(request.raw().getInputStream());
        } else {
            answer.setBody(request.body());
        }
        return answer;
    }

    @Override
    public void populateCamelHeaders(Request request, Map<String, Object> headers, Exchange exchange, SparkConfiguration configuration) throws Exception {
        // store the method and query and other info in headers as String types
        headers.put(Exchange.HTTP_METHOD, request.raw().getMethod());
        headers.put(Exchange.HTTP_QUERY, request.raw().getQueryString());
        headers.put(Exchange.HTTP_URL, request.raw().getRequestURL().toString());
        headers.put(Exchange.HTTP_URI, request.raw().getRequestURI());
        headers.put(Exchange.HTTP_PATH, request.raw().getPathInfo());
        headers.put(Exchange.CONTENT_TYPE, request.raw().getContentType());

        for (String key : request.attributes()) {
            Object value = request.attribute(key);
            Object decoded = shouldUrlDecodeHeader(configuration, key, value, "UTF-8");
            if (headerFilterStrategy != null
                    && !headerFilterStrategy.applyFilterToExternalHeaders(key, decoded, exchange)) {
                SparkHelper.appendHeader(headers, key, decoded);
            }
        }

        for (String key : request.headers()) {
            Object value = request.headers(key);
            Object decoded = shouldUrlDecodeHeader(configuration, key, value, "UTF-8");
            if (headerFilterStrategy != null
                    && !headerFilterStrategy.applyFilterToExternalHeaders(key, decoded, exchange)) {
                SparkHelper.appendHeader(headers, key, decoded);
            }
        }

        for (Map.Entry<String, String> entry : request.params().entrySet()) {
            String key = mapKey(entry.getKey());
            String value = entry.getValue();
            Object decoded = shouldUrlDecodeHeader(configuration, key, value, "UTF-8");
            if (headerFilterStrategy != null
                    && !headerFilterStrategy.applyFilterToExternalHeaders(key, decoded, exchange)) {
                SparkHelper.appendHeader(headers, key, decoded);
            }
        }

        String[] splat = request.splat();
        String key = SparkConstants.SPLAT;
        if (headerFilterStrategy != null
                && !headerFilterStrategy.applyFilterToExternalHeaders(key, splat, exchange)) {
            SparkHelper.appendHeader(headers, key, splat);
        }
    }

    @Override
    public void toSparkResponse(Message message, Response response, SparkConfiguration configuration) throws Exception {
        LOG.trace("toSparkResponse: {}", message);

        // the response code is 200 for OK and 500 for failed
        boolean failed = message.getExchange().isFailed();
        int defaultCode = failed ? 500 : 200;

        int code = message.getHeader(Exchange.HTTP_RESPONSE_CODE, defaultCode, int.class);
        response.status(code);
        LOG.trace("HTTP Status Code: {}", code);

        TypeConverter tc = message.getExchange().getContext().getTypeConverter();

        // append headers
        // must use entrySet to ensure case of keys is preserved
        for (Map.Entry<String, Object> entry : message.getHeaders().entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            // use an iterator as there can be multiple values. (must not use a delimiter)
            final Iterator<?> it = ObjectHelper.createIterator(value, null);
            while (it.hasNext()) {
                String headerValue = tc.convertTo(String.class, it.next());
                if (headerValue != null && headerFilterStrategy != null
                        && !headerFilterStrategy.applyFilterToCamelHeaders(key, headerValue, message.getExchange())) {
                    LOG.trace("HTTP-Header: {}={}", key, headerValue);
                    response.header(key, headerValue);
                }
            }
        }

        // set the content type in the response.
        String contentType = MessageHelper.getContentType(message);
        if (contentType != null) {
            // set content-type
            response.header(Exchange.CONTENT_TYPE, contentType);
            LOG.trace("Content-Type: {}", contentType);
        }

        Object body = message.getBody();
        Exception cause = message.getExchange().getException();

        // if there was an exception then use that as body
        if (cause != null) {
            if (configuration.isTransferException()) {
                // we failed due an exception, and transfer it as java serialized object
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(cause);
                oos.flush();
                IOHelper.close(oos, bos);

                body = bos.toByteArray();
                // force content type to be serialized java object
                message.setHeader(Exchange.CONTENT_TYPE, SparkConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT);
            } else {
                // we failed due an exception so print it as plain text
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                cause.printStackTrace(pw);

                // the body should then be the stacktrace
                body = sw.toString().getBytes();
                // force content type to be text/plain as that is what the stacktrace is
                message.setHeader(Exchange.CONTENT_TYPE, "text/plain");
            }

            // and mark the exception as failure handled, as we handled it by returning it as the response
            ExchangeHelper.setFailureHandled(message.getExchange());
        }

        if (body != null) {
            String str = tc.mandatoryConvertTo(String.class, message.getExchange(), body);
            response.body(str);
            // and must set body to the response body as Spark otherwise may output something else
            message.setBody(str);
        }
    }

    /**
     * Decodes the header if needed to, or returns the header value as is.
     *
     * @param configuration the configuration
     * @param headerName    the header name
     * @param value         the current header value
     * @param charset       the charset to use for decoding
     * @return the decoded value (if decoded was needed) or a <tt>toString</tt> representation of the value.
     * @throws java.io.UnsupportedEncodingException is thrown if error decoding.
     */
    protected String shouldUrlDecodeHeader(SparkConfiguration configuration, String headerName, Object value, String charset) throws
            UnsupportedEncodingException {
        // do not decode Content-Type
        if (Exchange.CONTENT_TYPE.equals(headerName)) {
            return value.toString();
        } else if (configuration.isUrlDecodeHeaders()) {
            return URLDecoder.decode(value.toString(), charset);
        } else {
            return value.toString();
        }
    }

    protected String mapKey(String key) {
        if (key.startsWith(":")) {
            return key.substring(1);
        } else {
            return key;
        }
    }

}
