/**
 * 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.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.xml.transform.Source;

import org.apache.camel.BytesSource;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.MessageHistory;
import org.apache.camel.StreamCache;
import org.apache.camel.StringSource;
import org.apache.camel.WrappedFile;
import org.apache.camel.spi.ExchangeFormatter;
import org.apache.camel.spi.HeaderFilterStrategy;

/**
 * Some helper methods when working with {@link org.apache.camel.Message}.
 * 
 * @version
 */
public final class MessageHelper {

    private static final String MESSAGE_HISTORY_HEADER = "%-20s %-20s %-80s %-12s";
    private static final String MESSAGE_HISTORY_OUTPUT = "[%-18.18s] [%-18.18s] [%-78.78s] [%10.10s]";

    /**
     * Utility classes should not have a public constructor.
     */
    private MessageHelper() {
    }

    /**
     * Extracts the given body and returns it as a String, that can be used for
     * logging etc.
     * <p/>
     * Will handle stream based bodies wrapped in StreamCache.
     * 
     * @param message the message with the body
     * @return the body as String, can return <tt>null</null> if no body
     */
    public static String extractBodyAsString(Message message) {
        if (message == null) {
            return null;
        }

        // optimize if the body is a String type already
        Object body = message.getBody();
        if (body instanceof String) {
            return (String) body;
        }

        // we need to favor using stream cache so the body can be re-read later
        StreamCache newBody = message.getBody(StreamCache.class);
        if (newBody != null) {
            message.setBody(newBody);
        }

        Object answer = message.getBody(String.class);
        if (answer == null) {
            answer = message.getBody();
        }

        if (newBody != null) {
            // Reset the InputStreamCache
            newBody.reset();
        }

        return answer != null ? answer.toString() : null;
    }

    /**
     * Gets the given body class type name as a String.
     * <p/>
     * Will skip java.lang. for the build in Java types.
     * 
     * @param message the message with the body
     * @return the body type name as String, can return
     *         <tt>null</null> if no body
     */
    public static String getBodyTypeName(Message message) {
        if (message == null) {
            return null;
        }
        String answer = ObjectHelper.classCanonicalName(message.getBody());
        if (answer != null && answer.startsWith("java.lang.")) {
            return answer.substring(10);
        }
        return answer;
    }

    /**
     * If the message body contains a {@link StreamCache} instance, reset the
     * cache to enable reading from it again.
     * 
     * @param message the message for which to reset the body
     */
    public static void resetStreamCache(Message message) {
        if (message == null) {
            return;
        }
        Object body = message.getBody();
        if (body instanceof StreamCache) {
            ((StreamCache) body).reset();
        }
    }

    /**
     * Returns the MIME content type on the message or <tt>null</tt> if none
     * defined
     */
    public static String getContentType(Message message) {
        return message.getHeader(Exchange.CONTENT_TYPE, String.class);
    }

    /**
     * Returns the MIME content encoding on the message or <tt>null</tt> if none
     * defined
     */
    public static String getContentEncoding(Message message) {
        return message.getHeader(Exchange.CONTENT_ENCODING, String.class);
    }

    /**
     * Extracts the body for logging purpose.
     * <p/>
     * Will clip the body if its too big for logging. Will prepend the message
     * with <tt>Message: </tt>
     * 
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param message the message
     * @return the logging message
     */
    public static String extractBodyForLogging(Message message) {
        return extractBodyForLogging(message, "Message: ");
    }

    /**
     * Extracts the value for logging purpose.
     * <p/>
     * Will clip the value if its too big for logging.
     *
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param value   the value
     * @param message the message
     * @return the logging message
     */
    public static String extractValueForLogging(Object value, Message message) {
        boolean streams = false;
        if (message.getExchange() != null) {
            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_STREAMS);
            if (globalOption != null) {
                streams = message.getExchange().getContext().getTypeConverter().convertTo(Boolean.class, message.getExchange(), globalOption);
            }
        }

        // default to 1000 chars
        int maxChars = 1000;

        if (message.getExchange() != null) {
            String property = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_MAX_CHARS);
            if (property != null) {
                maxChars = message.getExchange().getContext().getTypeConverter().convertTo(Integer.class, property);
            }
        }

        return extractValueForLogging(value, message, "", streams, false, maxChars);
    }

    /**
     * Extracts the body for logging purpose.
     * <p/>
     * Will clip the body if its too big for logging.
     *
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param message the message
     * @param prepend a message to prepend
     * @return the logging message
     */
    public static String extractBodyForLogging(Message message, String prepend) {
        boolean streams = false;
        if (message.getExchange() != null) {
            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_STREAMS);
            if (globalOption != null) {
                streams = message.getExchange().getContext().getTypeConverter().convertTo(Boolean.class, message.getExchange(), globalOption);
            }
        }
        return extractBodyForLogging(message, prepend, streams, false);
    }

    /**
     * Extracts the body for logging purpose.
     * <p/>
     * Will clip the body if its too big for logging.
     *
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param message the message
     * @param prepend a message to prepend
     * @param allowStreams whether or not streams is allowed
     * @param allowFiles whether or not files is allowed (currently not in use)
     * @return the logging message
     */
    public static String extractBodyForLogging(Message message, String prepend, boolean allowStreams, boolean allowFiles) {
        // default to 1000 chars
        int maxChars = 1000;

        if (message.getExchange() != null) {
            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_MAX_CHARS);
            if (globalOption != null) {
                maxChars = message.getExchange().getContext().getTypeConverter().convertTo(Integer.class, globalOption);
            }
        }

        return extractBodyForLogging(message, prepend, allowStreams, allowFiles, maxChars);
    }

    /**
     * Extracts the body for logging purpose.
     * <p/>
     * Will clip the body if its too big for logging.
     * 
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param message the message
     * @param prepend a message to prepend
     * @param allowStreams whether or not streams is allowed
     * @param allowFiles whether or not files is allowed (currently not in use)
     * @param maxChars limit to maximum number of chars. Use 0 for not limit, and -1 for turning logging message body off.
     * @return the logging message
     */
    public static String extractBodyForLogging(Message message, String prepend, boolean allowStreams, boolean allowFiles, int maxChars) {
        return extractValueForLogging(message.getBody(), message, prepend, allowStreams, allowFiles, maxChars);
    }

    /**
     * Extracts the value for logging purpose.
     * <p/>
     * Will clip the value if its too big for logging.
     *
     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
     * @param obj     the value
     * @param message the message
     * @param prepend a message to prepend
     * @param allowStreams whether or not streams is allowed
     * @param allowFiles whether or not files is allowed (currently not in use)
     * @param maxChars limit to maximum number of chars. Use 0 for not limit, and -1 for turning logging message body off.
     * @return the logging message
     */
    public static String extractValueForLogging(Object obj, Message message, String prepend, boolean allowStreams, boolean allowFiles, int maxChars) {
        if (maxChars < 0) {
            return prepend + "[Body is not logged]";
        }

        if (obj == null) {
            return prepend + "[Body is null]";
        }

        if (!allowStreams) {
            if (obj instanceof Source && !(obj instanceof StringSource || obj instanceof BytesSource)) {
                // for Source its only StringSource or BytesSource that is okay as they are memory based
                // all other kinds we should not touch the body
                return prepend + "[Body is instance of java.xml.transform.Source]";
            } else if (obj instanceof StreamCache) {
                return prepend + "[Body is instance of org.apache.camel.StreamCache]";
            } else if (obj instanceof InputStream) {
                return prepend + "[Body is instance of java.io.InputStream]";
            } else if (obj instanceof OutputStream) {
                return prepend + "[Body is instance of java.io.OutputStream]";
            } else if (obj instanceof Reader) {
                return prepend + "[Body is instance of java.io.Reader]";
            } else if (obj instanceof Writer) {
                return prepend + "[Body is instance of java.io.Writer]";
            } else if (obj instanceof WrappedFile || obj instanceof File) {
                if (!allowFiles) {
                    return prepend + "[Body is file based: " + obj + "]";
                }
            }
        }

        if (!allowFiles) {
            if (obj instanceof WrappedFile || obj instanceof File) {
                return prepend + "[Body is file based: " + obj + "]";
            }
        }

        // is the body a stream cache or input stream
        StreamCache cache = null;
        InputStream is = null;
        if (obj instanceof StreamCache) {
            cache = (StreamCache)obj;
            is = null;
        } else if (obj instanceof InputStream) {
            cache = null;
            is = (InputStream) obj;
        }

        // grab the message body as a string
        String body = null;
        if (message.getExchange() != null) {
            try {
                body = message.getExchange().getContext().getTypeConverter().tryConvertTo(String.class, message.getExchange(), obj);
            } catch (Throwable e) {
                // ignore as the body is for logging purpose
            }
        }
        if (body == null) {
            try {
                body = obj.toString();
            } catch (Throwable e) {
                // ignore as the body is for logging purpose
            }
        }

        // reset stream cache after use
        if (cache != null) {
            cache.reset();
        } else if (is != null && is.markSupported()) {
            try {
                is.reset();
            } catch (IOException e) {
                // ignore
            }
        }

        if (body == null) {
            return prepend + "[Body is null]";
        }

        // clip body if length enabled and the body is too big
        if (maxChars > 0 && body.length() > maxChars) {
            body = body.substring(0, maxChars) + "... [Body clipped after " + maxChars + " chars, total length is " + body.length() + "]";
        }

        return prepend + body;
    }

    /**
     * Dumps the message as a generic XML structure.
     * 
     * @param message the message
     * @return the XML
     */
    public static String dumpAsXml(Message message) {
        return dumpAsXml(message, true);
    }

    /**
     * Dumps the message as a generic XML structure.
     * 
     * @param message the message
     * @param includeBody whether or not to include the message body
     * @return the XML
     */
    public static String dumpAsXml(Message message, boolean includeBody) {
        return dumpAsXml(message, includeBody, 0);
    }

    /**
     * Dumps the message as a generic XML structure.
     *
     * @param message the message
     * @param includeBody whether or not to include the message body
     * @param indent number of spaces to indent
     * @return the XML
     */
    public static String dumpAsXml(Message message, boolean includeBody, int indent) {
        return dumpAsXml(message, includeBody, indent, false, true, 128 * 1024);
    }

    /**
     * Dumps the message as a generic XML structure.
     *
     * @param message the message
     * @param includeBody whether or not to include the message body
     * @param indent number of spaces to indent
     * @param allowStreams whether to include message body if they are stream based
     * @param allowFiles whether to include message body if they are file based
     * @param maxChars clip body after maximum chars (to avoid very big messages). Use 0 or negative value to not limit at all.
     * @return the XML
     */
    public static String dumpAsXml(Message message, boolean includeBody, int indent, boolean allowStreams, boolean allowFiles, int maxChars) {
        StringBuilder sb = new StringBuilder();

        StringBuilder prefix = new StringBuilder();
        for (int i = 0; i < indent; i++) {
            prefix.append(" ");
        }

        // include exchangeId as attribute on the <message> tag
        sb.append(prefix);
        sb.append("<message exchangeId=\"").append(message.getExchange().getExchangeId()).append("\">\n");

        // headers
        if (message.hasHeaders()) {
            sb.append(prefix);
            sb.append("  <headers>\n");
            // sort the headers so they are listed A..Z
            Map<String, Object> headers = new TreeMap<>(message.getHeaders());
            for (Map.Entry<String, Object> entry : headers.entrySet()) {
                Object value = entry.getValue();
                String type = ObjectHelper.classCanonicalName(value);
                sb.append(prefix);
                sb.append("    <header key=\"").append(entry.getKey()).append("\"");
                if (type != null) {
                    sb.append(" type=\"").append(type).append("\"");
                }
                sb.append(">");

                // dump header value as XML, use Camel type converter to convert
                // to String
                if (value != null) {
                    try {
                        String xml = message.getExchange().getContext().getTypeConverter().tryConvertTo(String.class,
                                message.getExchange(), value);
                        if (xml != null) {
                            // must always xml encode
                            sb.append(StringHelper.xmlEncode(xml));
                        }
                    } catch (Throwable e) {
                        // ignore as the body is for logging purpose
                    }
                }

                sb.append("</header>\n");
            }
            sb.append(prefix);
            sb.append("  </headers>\n");
        }

        if (includeBody) {
            sb.append(prefix);
            sb.append("  <body");
            String type = ObjectHelper.classCanonicalName(message.getBody());
            if (type != null) {
                sb.append(" type=\"").append(type).append("\"");
            }
            sb.append(">");

            String xml = extractBodyForLogging(message, "", allowStreams, allowFiles, maxChars);
            if (xml != null) {
                // must always xml encode
                sb.append(StringHelper.xmlEncode(xml));
            }

            sb.append("</body>\n");
        }

        sb.append(prefix);
        sb.append("</message>");
        return sb.toString();
    }

    /**
     * Copies the headers from the source to the target message.
     * 
     * @param source the source message
     * @param target the target message
     * @param override whether to override existing headers
     */
    public static void copyHeaders(Message source, Message target, boolean override) {
        copyHeaders(source, target, null, override);
    }
    
    /**
     * Copies the headers from the source to the target message.
     * 
     * @param source the source message
     * @param target the target message
     * @param strategy the header filter strategy which could help us to filter the protocol message headers
     * @param override whether to override existing headers
     */
    public static void copyHeaders(Message source, Message target, HeaderFilterStrategy strategy, boolean override) {
        if (!source.hasHeaders()) {
            return;
        }

        for (Map.Entry<String, Object> entry : source.getHeaders().entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (target.getHeader(key) == null || override) {
                if (strategy == null) {
                    target.setHeader(key, value);
                } else if (!strategy.applyFilterToExternalHeaders(key, value, target.getExchange())) {
                    // Just make sure we don't copy the protocol headers to target
                    target.setHeader(key, value);
                }
            }
        }
    }

    /**
     * Dumps the {@link MessageHistory} from the {@link Exchange} in a human readable format.
     *
     * @param exchange           the exchange
     * @param exchangeFormatter  if provided then information about the exchange is included in the dump
     * @param logStackTrace      whether to include a header for the stacktrace, to be added (not included in this dump).
     * @return a human readable message history as a table
     */
    public static String dumpMessageHistoryStacktrace(Exchange exchange, ExchangeFormatter exchangeFormatter, boolean logStackTrace) {
        // must not cause new exceptions so run this in a try catch block
        try {
            return doDumpMessageHistoryStacktrace(exchange, exchangeFormatter, logStackTrace);
        } catch (Throwable e) {
            // ignore as the body is for logging purpose
            return "";
        }
    }

    @SuppressWarnings("unchecked")
    public static String doDumpMessageHistoryStacktrace(Exchange exchange, ExchangeFormatter exchangeFormatter, boolean logStackTrace) {
        List<MessageHistory> list = exchange.getProperty(Exchange.MESSAGE_HISTORY, List.class);
        if (list == null || list.isEmpty()) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        sb.append("\n");
        sb.append("Message History\n");
        sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n");
        String goMessageHistoryHeaeder = exchange.getContext().getGlobalOption(Exchange.MESSAGE_HISTORY_HEADER_FORMAT);
        sb.append(String.format(
                         goMessageHistoryHeaeder == null ? MESSAGE_HISTORY_HEADER : goMessageHistoryHeaeder,
                         "RouteId", "ProcessorId", "Processor", "Elapsed (ms)"));
        sb.append("\n");

        // add incoming origin of message on the top
        String routeId = exchange.getFromRouteId();
        String id = routeId;
        String label = "";
        if (exchange.getFromEndpoint() != null) {
            label = URISupport.sanitizeUri(exchange.getFromEndpoint().getEndpointUri());
        }
        long elapsed = 0;
        Date created = exchange.getCreated();
        if (created != null) {
            elapsed = new StopWatch(created).taken();
        }

        String goMessageHistoryOutput = exchange.getContext().getGlobalOption(Exchange.MESSAGE_HISTORY_OUTPUT_FORMAT);
        goMessageHistoryOutput = goMessageHistoryOutput == null ? MESSAGE_HISTORY_OUTPUT : goMessageHistoryOutput;
        sb.append(String.format(goMessageHistoryOutput, routeId, id, label, elapsed));
        sb.append("\n");

        // and then each history
        for (MessageHistory history : list) {
            routeId = history.getRouteId() != null ? history.getRouteId() : "";
            id = history.getNode().getId();
            // we need to avoid leak the sensible information here
            // the sanitizeUri takes a very long time for very long string and the format cuts this to
            // 78 characters, anyway. Cut this to 100 characters. This will give enough space for removing
            // characters in the sanitizeUri method and will be reasonably fast
            label =  URISupport.sanitizeUri(StringHelper.limitLength(history.getNode().getLabel(), 100));
            elapsed = history.getElapsed();

            sb.append(String.format(goMessageHistoryOutput, routeId, id, label, elapsed));
            sb.append("\n");
        }

        if (exchangeFormatter != null) {
            sb.append("\nExchange\n");
            sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n");
            sb.append(exchangeFormatter.format(exchange));
            sb.append("\n");
        }

        if (logStackTrace) {
            sb.append("\nStacktrace\n");
            sb.append("---------------------------------------------------------------------------------------------------------------------------------------");
        }
        return sb.toString();
    }

}
