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

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Future;

import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.spi.ExchangeFormatter;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriParams;
import org.apache.camel.util.MessageHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.StringHelper;

/**
 * Default {@link ExchangeFormatter} that have fine grained options to configure what to include in the output.
 */
@UriParams
public class DefaultExchangeFormatter implements ExchangeFormatter {

    protected static final String LS = System.getProperty("line.separator");
    private static final String SEPARATOR = "###REPLACE_ME###";

    public enum OutputStyle { Default, Tab, Fixed }

    @UriParam(label = "formatting")
    private boolean showExchangeId;
    @UriParam(label = "formatting", defaultValue = "true")
    private boolean showExchangePattern = true;
    @UriParam(label = "formatting")
    private boolean showProperties;
    @UriParam(label = "formatting")
    private boolean showHeaders;
    @UriParam(label = "formatting", defaultValue = "true")
    private boolean skipBodyLineSeparator = true;
    @UriParam(label = "formatting", defaultValue = "true", description = "Show the message body.")
    private boolean showBody = true;
    @UriParam(label = "formatting", defaultValue = "true")
    private boolean showBodyType = true;
    @UriParam(label = "formatting")
    private boolean showOut;
    @UriParam(label = "formatting")
    private boolean showException;
    @UriParam(label = "formatting")
    private boolean showCaughtException;
    @UriParam(label = "formatting")
    private boolean showStackTrace;
    @UriParam(label = "formatting")
    private boolean showAll;
    @UriParam(label = "formatting")
    private boolean multiline;
    @UriParam(label = "formatting")
    private boolean showFuture;
    @UriParam(label = "formatting")
    private boolean showStreams;
    @UriParam(label = "formatting")
    private boolean showFiles;
    @UriParam(label = "formatting", defaultValue = "10000")
    private int maxChars = 10000;
    @UriParam(label = "formatting", enums = "Default,Tab,Fixed", defaultValue = "Default")
    private OutputStyle style = OutputStyle.Default;

    private String style(String label) {
        if (style == OutputStyle.Default) {
            return String.format(", %s: ", label);
        } 
        if (style == OutputStyle.Tab) {
            return String.format("\t%s: ", label);
        } else {
            return String.format("\t%-20s", label);
        }
    }

    public String format(Exchange exchange) {
        Message in = exchange.getIn();

        StringBuilder sb = new StringBuilder();
        if (showAll || showExchangeId) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            sb.append(style("Id")).append(exchange.getExchangeId());
        }
        if (showAll || showExchangePattern) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            sb.append(style("ExchangePattern")).append(exchange.getPattern());
        }

        if (showAll || showProperties) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            sb.append(style("Properties")).append(sortMap(exchange.getProperties()));
        }
        if (showAll || showHeaders) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            sb.append(style("Headers")).append(sortMap(in.getHeaders()));
        }
        if (showAll || showBodyType) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            sb.append(style("BodyType")).append(getBodyTypeAsString(in));
        }
        if (showAll || showBody) {
            if (multiline) {
                sb.append(SEPARATOR);
            }
            String body = getBodyAsString(in);
            if (skipBodyLineSeparator) {
                body = StringHelper.replaceAll(body, LS, "");
            }
            sb.append(style("Body")).append(body);
        }

        if (showAll || showException || showCaughtException) {

            // try exception on exchange first
            Exception exception = exchange.getException();
            boolean caught = false;
            if ((showAll || showCaughtException) && exception == null) {
                // fallback to caught exception
                exception = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
                caught = true;
            }

            if (exception != null) {
                if (multiline) {
                    sb.append(SEPARATOR);
                }
                if (caught) {
                    sb.append(style("CaughtExceptionType")).append(exception.getClass().getCanonicalName());
                    sb.append(style("CaughtExceptionMessage")).append(exception.getMessage());
                } else {
                    sb.append(style("ExceptionType")).append(exception.getClass().getCanonicalName());
                    sb.append(style("ExceptionMessage")).append(exception.getMessage());
                }
                if (showAll || showStackTrace) {
                    StringWriter sw = new StringWriter();
                    exception.printStackTrace(new PrintWriter(sw));
                    sb.append(style("StackTrace")).append(sw.toString());
                }
            }
        }

        if (showAll || showOut) {
            if (exchange.hasOut()) {
                Message out = exchange.getOut();
                if (showAll || showHeaders) {
                    if (multiline) {
                        sb.append(SEPARATOR);
                    }
                    sb.append(style("OutHeaders")).append(sortMap(out.getHeaders()));
                }
                if (showAll || showBodyType) {
                    if (multiline) {
                        sb.append(SEPARATOR);
                    }
                    sb.append(style("OutBodyType")).append(getBodyTypeAsString(out));
                }
                if (showAll || showBody) {
                    if (multiline) {
                        sb.append(SEPARATOR);
                    }
                    String body = getBodyAsString(out);
                    if (skipBodyLineSeparator) {
                        body = StringHelper.replaceAll(body, LS, "");
                    }
                    sb.append(style("OutBody")).append(body);
                }
            } else {
                if (multiline) {
                    sb.append(SEPARATOR);
                }
                sb.append(style("Out: null"));
            }
        }

        if (maxChars > 0) {
            StringBuilder answer = new StringBuilder();
            for (String s : sb.toString().split(SEPARATOR)) {
                if (s != null) {
                    if (s.length() > maxChars) {
                        s = s.substring(0, maxChars);
                        answer.append(s).append("...");
                    } else {
                        answer.append(s);
                    }
                    if (multiline) {
                        answer.append(LS);
                    }
                }
            }

            // switch string buffer
            sb = answer;
        }

        if (multiline) {
            sb.insert(0, "Exchange[");
            sb.append("]");
            return sb.toString();
        } else {
            // get rid of the leading space comma if needed
            if (sb.length() > 0 && sb.charAt(0) == ',' && sb.charAt(1) == ' ') {
                sb.replace(0, 2, "");
            }
            sb.insert(0, "Exchange[");
            sb.append("]");

            return sb.toString();
        }
    }

    public boolean isShowExchangeId() {
        return showExchangeId;
    }

    /**
     * Show the unique exchange ID.
     */
    public void setShowExchangeId(boolean showExchangeId) {
        this.showExchangeId = showExchangeId;
    }

    public boolean isShowProperties() {
        return showProperties;
    }

    /**
     * Show the exchange properties.
     */
    public void setShowProperties(boolean showProperties) {
        this.showProperties = showProperties;
    }

    public boolean isShowHeaders() {
        return showHeaders;
    }

    /**
     * Show the message headers.
     */
    public void setShowHeaders(boolean showHeaders) {
        this.showHeaders = showHeaders;
    }

    public boolean isSkipBodyLineSeparator() {
        return skipBodyLineSeparator;
    }

    /**
     * Whether to skip line separators when logging the message body.
     * This allows to log the message body in one line, setting this option to false will preserve any line separators
     * from the body, which then will log the body as is.
     */
    public void setSkipBodyLineSeparator(boolean skipBodyLineSeparator) {
        this.skipBodyLineSeparator = skipBodyLineSeparator;
    }

    public boolean isShowBodyType() {
        return showBodyType;
    }

    /**
     * Show the body Java type.
     */
    public void setShowBodyType(boolean showBodyType) {
        this.showBodyType = showBodyType;
    }

    public boolean isShowBody() {
        return showBody;
    }

    /*
     * Show the message body.
     */
    public void setShowBody(boolean showBody) {
        this.showBody = showBody;
    }

    public boolean isShowOut() {
        return showOut;
    }

    /**
     * If the exchange has an out message, show the out message.
     */
    public void setShowOut(boolean showOut) {
        this.showOut = showOut;
    }

    public boolean isShowAll() {
        return showAll;
    }

    /**
     * Quick option for turning all options on. (multiline, maxChars has to be manually set if to be used)
     */
    public void setShowAll(boolean showAll) {
        this.showAll = showAll;
    }

    public boolean isShowException() {
        return showException;
    }

    /**
     * If the exchange has an exception, show the exception message (no stacktrace)
     */
    public void setShowException(boolean showException) {
        this.showException = showException;
    }

    public boolean isShowStackTrace() {
        return showStackTrace;
    }

    /**
     * Show the stack trace, if an exchange has an exception. Only effective if one of showAll, showException or showCaughtException are enabled.
     */
    public void setShowStackTrace(boolean showStackTrace) {
        this.showStackTrace = showStackTrace;
    }

    public boolean isShowCaughtException() {
        return showCaughtException;
    }

    /**
     * f the exchange has a caught exception, show the exception message (no stack trace).
     * A caught exception is stored as a property on the exchange (using the key {@link org.apache.camel.Exchange#EXCEPTION_CAUGHT}
     * and for instance a doCatch can catch exceptions.
     */
    public void setShowCaughtException(boolean showCaughtException) {
        this.showCaughtException = showCaughtException;
    }

    public boolean isMultiline() {
        return multiline;
    }

    public int getMaxChars() {
        return maxChars;
    }

    /**
     * Limits the number of characters logged per line.
     */
    public void setMaxChars(int maxChars) {
        this.maxChars = maxChars;
    }

    /**
     * If enabled then each information is outputted on a newline.
     */
    public void setMultiline(boolean multiline) {
        this.multiline = multiline;
    }

    public boolean isShowFuture() {
        return showFuture;
    }

    /**
     * If enabled Camel will on Future objects wait for it to complete to obtain the payload to be logged.
     */
    public void setShowFuture(boolean showFuture) {
        this.showFuture = showFuture;
    }

    public boolean isShowExchangePattern() {
        return showExchangePattern;
    }

    /**
     * Shows the Message Exchange Pattern (or MEP for short).
     */
    public void setShowExchangePattern(boolean showExchangePattern) {
        this.showExchangePattern = showExchangePattern;
    }

    public boolean isShowStreams() {
        return showStreams;
    }

    /**
     * Whether Camel should show stream bodies or not (eg such as java.io.InputStream).
     * Beware if you enable this option then you may not be able later to access the message body
     * as the stream have already been read by this logger.
     * To remedy this you will have to use Stream Caching.
     */
    public void setShowStreams(boolean showStreams) {
        this.showStreams = showStreams;
    }

    public boolean isShowFiles() {
        return showFiles;
    }

    /**
     * If enabled Camel will output files
     */
    public void setShowFiles(boolean showFiles) {
        this.showFiles = showFiles;
    }

    public OutputStyle getStyle() {
        return style;
    }

    /**
     * Sets the outputs style to use.
     */
    public void setStyle(OutputStyle style) {
        this.style = style;
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    protected String getBodyAsString(Message message) {
        if (message.getBody() instanceof Future) {
            if (!isShowFuture()) {
                // just use a to string of the future object
                return message.getBody().toString();
            }
        }

        return MessageHelper.extractBodyForLogging(message, "", isShowStreams(), isShowFiles(), getMaxChars(message));
    }

    private int getMaxChars(Message message) {
        int maxChars = getMaxChars();
        if (message.getExchange() != null) {
            String property = message.getExchange().getContext().getProperty(Exchange.LOG_DEBUG_BODY_MAX_CHARS);
            if (property != null) {
                maxChars = message.getExchange().getContext().getTypeConverter().convertTo(Integer.class, property);
            }
        }
        return maxChars;
    }

    protected String getBodyTypeAsString(Message message) {
        String answer = ObjectHelper.classCanonicalName(message.getBody());
        if (answer != null && answer.startsWith("java.lang.")) {
            return answer.substring(10);
        }
        return answer;
    }

    private static Map<String, Object> sortMap(Map<String, Object> map) {
        Map<String, Object> answer = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
        answer.putAll(map);
        return answer;
    }

}
