/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2014 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.message.internal;

import java.lang.reflect.Method;
import java.util.logging.Logger;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import javax.annotation.Priority;

import org.glassfish.jersey.internal.PropertiesDelegate;

/**
 * Low level Tracing support API.
 *
 * Use one instance per request.
 *
 * @author Libor Kramolis (libor.kramolis at oracle.com)
 * @since 2.3
 */
public abstract class TracingLogger {

    /**
     * {@code TracingLogger} instance is placed in request context properties under this name.
     */
    public static final String PROPERTY_NAME = TracingLogger.class.getName();
    /**
     * HTTP header prefix.
     */
    private static final String HEADER_TRACING_PREFIX = "X-Jersey-Tracing-";
    /**
     * Request header name to change application default tracing level.
     */
    public static final String HEADER_THRESHOLD = HEADER_TRACING_PREFIX + "Threshold";
    /**
     * Request header name to switch on request tracing.
     * Make sense in case of tracing support enabled by ON_DEMAND value.
     */
    public static final String HEADER_ACCEPT = HEADER_TRACING_PREFIX + "Accept";
    /**
     * Request header name to set JDK logger name suffix to identify a request logs.
     */
    public static final String HEADER_LOGGER = HEADER_TRACING_PREFIX + "Logger";
    /**
     * Response header name format.
     */
    private static final String HEADER_RESPONSE_FORMAT = HEADER_TRACING_PREFIX + "%03d";
    /**
     * Default event level.
     */
    public static final Level DEFAULT_LEVEL = Level.TRACE;
    /**
     * JDK logger name prefix.
     */
    private static final String TRACING_LOGGER_NAME_PREFIX = "org.glassfish.jersey.tracing";
    /**
     * Default JDK logger name suffix. This can be overwrite by header {@link #HEADER_LOGGER}.
     */
    private static final String DEFAULT_LOGGER_NAME_SUFFIX = "general";
    /**
     * Empty (no-op) tracing logger.
     */
    private static final TracingLogger EMPTY = new TracingLogger() {

        @Override
        public boolean isLogEnabled(Event event) {
            return false;
        }

        @Override
        public void log(Event event, Object... args) {
            // no-op
        }

        @Override
        public void logDuration(Event event, long fromTimestamp, Object... args) {
            // no-op
        }

        @Override
        public long timestamp(Event event) {
            return -1;
        }

        @Override
        public void flush(MultivaluedMap<String, Object> headers) {
            // no-op
        }
    };

    /**
     * Returns instance of {@code TracingLogger} associated with current request processing
     * ({@code propertiesDelegate}).
     *
     * @param propertiesDelegate request associated runtime properties. Can be {@code null} if not running on server side.
     * @return returns instance of {@code TracingLogger} from {@code propertiesDelegate}. Does not return {@code null}.
     */
    // TODO look for places where getInstance(RequestProcessingContext) would make sense
    public static TracingLogger getInstance(PropertiesDelegate propertiesDelegate) {
        if (propertiesDelegate == null) {
            //not server side
            return EMPTY;
        }
        final TracingLogger tracingLogger = (TracingLogger) propertiesDelegate.getProperty(PROPERTY_NAME);
        return (tracingLogger != null) ? tracingLogger : EMPTY;
    }

    /**
     * Create new Tracing logger.
     *
     * @param threshold        tracing level threshold.
     * @param loggerNameSuffix tracing logger name suffix.
     * @return new tracing logger.
     */
    public static TracingLogger create(Level threshold, String loggerNameSuffix) {
        return new TracingLoggerImpl(threshold, loggerNameSuffix);
    }

    /**
     * Get an empty (no-op) tracing logger instance.
     *
     * @return empty tracing logger instance.
     */
    public static TracingLogger empty() {
        return EMPTY;
    }

    /**
     * Test if a tracing support is enabled (according to {@code propertiesDelegate} setting) and
     * if {@code event} can be logged (according to {@code event.level} and threshold level set).
     *
     * @param event event type to be tested
     * @return {@code true} if {@code event} can be logged
     */
    public abstract boolean isLogEnabled(Event event);

    /**
     * Try to log event according to event level and request context threshold level setting.
     *
     * @param event event type to be logged
     * @param args  message arguments (in relation to {@link org.glassfish.jersey.message.internal.TracingLogger.Event#messageFormat()}
     */
    public abstract void log(Event event, Object... args);

    /**
     * Try to log event according to event level and request context threshold level setting.
     *
     * If logging support is switched on for current request and event setting the method computes duration of event and log
     * message. If {@code fromTimestamp} is not set (i.e. {@code -1}) then duration of event
     * is {@code 0}.
     *
     * @param event         event type to be logged
     * @param fromTimestamp logged event is running from the timestamp in nanos. {@code -1} in case event has no duration
     * @param args          message arguments (in relation to {@link org.glassfish.jersey.message.internal.TracingLogger.Event#messageFormat()}
     */
    public abstract void logDuration(Event event, long fromTimestamp, Object... args);

    /**
     * If logging support is switched on for current request and event setting the method returns current timestamp in nanos.
     *
     * @param event event type to be logged
     * @return Current timestamp in nanos or {@code -1} if tracing is not enabled
     */
    public abstract long timestamp(Event event);

    /**
     * Stores collected tracing messages to response HTTP header.
     *
     * @param headers message headers.
     */
    public abstract void flush(MultivaluedMap<String, Object> headers);

    /**
     * Real implementation of tracing logger.
     */
    private static final class TracingLoggerImpl extends TracingLogger {

        private final Logger logger;
        private final Level threshold;
        private final TracingInfo tracingInfo;

        public TracingLoggerImpl(Level threshold, String loggerNameSuffix) {
            this.threshold = threshold;

            this.tracingInfo = new TracingInfo();

            loggerNameSuffix = loggerNameSuffix != null ? loggerNameSuffix : DEFAULT_LOGGER_NAME_SUFFIX;
            this.logger = Logger.getLogger(TRACING_LOGGER_NAME_PREFIX + "." + loggerNameSuffix);
        }

        @Override
        public boolean isLogEnabled(Event event) {
            return isEnabled(event.level());
        }

        @Override
        public void log(Event event, Object... args) {
            logDuration(event, -1, args);
        }

        @Override
        public void logDuration(Event event, long fromTimestamp, Object... args) {
            if (isEnabled(event.level())) {
                final long toTimestamp;
                if (fromTimestamp == -1) {
                    toTimestamp = -1;
                } else {
                    toTimestamp = System.nanoTime();
                }
                long duration = 0;
                if ((fromTimestamp != -1) && (toTimestamp != -1)) {
                    duration = toTimestamp - fromTimestamp;
                }
                logImpl(event, duration, args);
            }
        }

        @Override
        public long timestamp(Event event) {
            if (isEnabled(event.level())) {
                return System.nanoTime();
            }
            return -1;
        }

        @Override
        public void flush(final MultivaluedMap<String, Object> headers) {
            final String[] messages = tracingInfo.getMessages();
            for (int i = 0; i < messages.length; i++) {
                headers.putSingle(String.format(TracingLogger.HEADER_RESPONSE_FORMAT, i), messages[i]);
            }
        }

        /**
         * Log message for specified event type.
         *
         * The event contains name, category, level and also message format.
         * Message format will be formatted by parameter {@code messageArgs} is used to format event.
         * If there is no message format then each message arg is separated by space.
         * Final message also contains event name (JDK Log) or category (HTTP header) and time stamp.
         *
         * @param event       Event type of log
         * @param duration    Time duration of logged event. Can be {@code 0}.
         * @param messageArgs message arguments
         */
        private void logImpl(final Event event, final long duration, final Object... messageArgs) {
            if (isEnabled(event.level())) {
                final String[] messageArgsStr = new String[messageArgs.length];
                for (int i = 0; i < messageArgs.length; i++) {
                    messageArgsStr[i] = formatInstance(messageArgs[i]);
                }
                final TracingInfo.Message message = new TracingInfo.Message(event, duration, messageArgsStr);
                tracingInfo.addMessage(message);

                final java.util.logging.Level loggingLevel;
                switch (event.level()) {
                    case SUMMARY:
                        loggingLevel = java.util.logging.Level.FINE;
                        break;
                    case TRACE:
                        loggingLevel = java.util.logging.Level.FINER;
                        break;
                    case VERBOSE:
                        loggingLevel = java.util.logging.Level.FINEST;
                        break;
                    default:
                        loggingLevel = java.util.logging.Level.OFF;
                }
                if (logger.isLoggable(loggingLevel)) {
                    StringBuilder textSB = new StringBuilder();
                    textSB.append(event.name()).append(' ').append(message.toString());
                    textSB.append(" [").append(TracingInfo.formatDuration(duration)).append(" ms]");
                    logger.log(loggingLevel, textSB.toString());
                }
            }
        }

        private boolean isEnabled(Level level) {
            return threshold.ordinal() >= level.ordinal();
        }

        /**
         * Format info of instance.
         *
         * It shows its class name, identity hash code, and following info if available: priority value, response detail.
         *
         * @param instance instance to be formatted
         * @return Formatted info of instance.
         */
        private static String formatInstance(Object instance) {
            final StringBuilder textSB = new StringBuilder();
            if (instance == null) {
                textSB.append("null");
            } else if ((instance instanceof Number) || (instance instanceof String) || (instance instanceof Method)) {
                textSB.append(instance.toString());
            } else if (instance instanceof Response.StatusType) {
                textSB.append(formatStatusInfo((Response.StatusType) instance));
            } else {
                textSB.append('[');
                formatInstance(instance, textSB);
                if (instance.getClass().isAnnotationPresent(Priority.class)) {
                    textSB.append(" #").append(instance.getClass().getAnnotation(Priority.class).value());
                }
                if (instance instanceof WebApplicationException) {
                    formatResponse(((WebApplicationException) instance).getResponse(), textSB);
                } else if (instance instanceof Response) {
                    formatResponse(((Response) instance), textSB);
                }
                textSB.append(']');
            }
            return textSB.toString();
        }

        /**
         * Basic format of instance - just class name and identity hash code.
         *
         * @param instance instance to be formatted
         * @param textSB   Formatted info will be appended to {@code StringBuilder}
         */
        private static void formatInstance(Object instance, StringBuilder textSB) {
            textSB.append(instance.getClass().getName()).append(" @").
                    append(Integer.toHexString(System.identityHashCode(instance)));
        }

        /**
         * Format of response - status code, status family, reason phrase and info about entity.
         *
         * @param response response to be formatted
         * @param textSB   Formatted info will be appended to {@code StringBuilder}
         */
        private static void formatResponse(Response response, StringBuilder textSB) {
            textSB.append(" <").append(formatStatusInfo(response.getStatusInfo())).append('|');
            if (response.hasEntity()) {
                formatInstance(response.getEntity(), textSB);
            } else {
                textSB.append("-no-entity-");
            }
            textSB.append('>');
        }

        private static String formatStatusInfo(Response.StatusType statusInfo) {
            final StringBuilder textSB = new StringBuilder();

            textSB.append(statusInfo.getStatusCode()).append('/').
                    append(statusInfo.getFamily()).append('|').
                    append(statusInfo.getReasonPhrase());

            return textSB.toString();
        }
    }


    /**
     * Level of tracing message.
     */
    public static enum Level {
        /**
         * Brief tracing information level.
         */
        SUMMARY,
        /**
         * Detailed tracing information level.
         */
        TRACE,
        /**
         * Extremely detailed tracing information level.
         */
        VERBOSE
    }

    /**
     * Type of event.
     */
    public static interface Event {
        /**
         * Name of event, should be unique.
         * Is logged by JDK logger.
         *
         * @return event name.
         */
        public String name();

        /**
         * Category of event, more events share same category.
         * Is used to format response HTTP header.
         *
         * @return event category.
         */
        public String category();

        /**
         * Level of event.
         * Is used to check if the event is logged according to application/request settings.
         *
         * @return event trace level.
         */
        public Level level();

        /**
         * Message format. Use {@link String#format(String, Object...)} format.
         * Can be null. In that case message arguments are separated by space
         * (see {@link TracingLoggerImpl#logImpl(Event, long, Object...)}.
         *
         * @return message format
         */
        public String messageFormat();
    }

}
