/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-2017 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.server.internal.monitoring;

import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.ws.rs.ProcessingException;

import javax.annotation.Priority;
import javax.inject.Inject;

import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.DestroyListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.glassfish.jersey.uri.UriTemplate;

/**
 * {@link ApplicationEventListener application event listener} that listens to {@link ApplicationEvent application}
 * and {@link RequestEvent request} events and supplies data to {@link MonitoringStatisticsProcessor} which
 * produces {@link org.glassfish.jersey.server.monitoring.MonitoringStatistics monitoring statistics}.
 * <p>
 * The {@link MonitoringStatisticsProcessor} is started by this class after the first application event
 * comes.
 * </p>
 * <p>
 * This event listener must be registered as a standard provider when monitoring statistics are required
 * in the runtime.
 * </p>
 *
 * @author Miroslav Fuksa
 * @see MonitoringStatisticsProcessor
 */
@Priority(ApplicationInfoListener.PRIORITY + 100)
public final class MonitoringEventListener implements ApplicationEventListener {

    private static final Logger LOGGER = Logger.getLogger(MonitoringEventListener.class.getName());
    private static final int EVENT_QUEUE_SIZE = 500_000;

    @Inject
    private InjectionManager injectionManager;

    private final Queue<RequestStats> requestQueuedItems = new ArrayBlockingQueue<>(EVENT_QUEUE_SIZE);
    private final Queue<Integer> responseStatuses = new ArrayBlockingQueue<>(EVENT_QUEUE_SIZE);
    private final Queue<RequestEvent> exceptionMapperEvents = new ArrayBlockingQueue<>(EVENT_QUEUE_SIZE);
    private volatile MonitoringStatisticsProcessor monitoringStatisticsProcessor;

    /**
     * Time statistics.
     */
    static class TimeStats {

        private final long duration;
        private final long startTime;

        private TimeStats(final long startTime, final long requestDuration) {
            this.duration = requestDuration;
            this.startTime = startTime;
        }

        /**
         * Get duration.
         *
         * @return Duration in milliseconds.
         */
        long getDuration() {
            return duration;
        }

        /**
         * Get start time.
         *
         * @return Start time (Unix timestamp format).
         */
        long getStartTime() {
            return startTime;
        }
    }

    /**
     * Method statistics.
     */
    static class MethodStats extends TimeStats {

        private final ResourceMethod method;

        private MethodStats(final ResourceMethod method, final long startTime, final long requestDuration) {
            super(startTime, requestDuration);
            this.method = method;
        }

        /**
         * Get the resource method executed.
         *
         * @return resource method.
         */
        ResourceMethod getMethod() {
            return method;
        }
    }

    /**
     * Request statistics.
     */
    static class RequestStats {

        private final TimeStats requestStats;
        private final MethodStats methodStats; // might be null if a method was not executed during a request
        private final String requestUri;

        private RequestStats(final TimeStats requestStats, final MethodStats methodStats, final String requestUri) {
            this.requestStats = requestStats;
            this.methodStats = methodStats;
            this.requestUri = requestUri;
        }

        /**
         * Get request statistics.
         *
         * @return request statistics.
         */
        TimeStats getRequestStats() {
            return requestStats;
        }

        /**
         * Get method statistics.
         *
         * @return method statistics.
         */
        MethodStats getMethodStats() {
            return methodStats;
        }

        /**
         * Get the request uri.
         *
         * @return request uri.
         */
        String getRequestUri() {
            return requestUri;
        }
    }

    @Override
    public ReqEventListener onRequest(final RequestEvent requestEvent) {
        switch (requestEvent.getType()) {
            case START:
                return new ReqEventListener();

        }
        return null;
    }

    @Override
    public void onEvent(final ApplicationEvent event) {
        final ApplicationEvent.Type type = event.getType();
        switch (type) {
            case INITIALIZATION_START:
                break;
            case RELOAD_FINISHED:
            case INITIALIZATION_FINISHED:
                this.monitoringStatisticsProcessor = new MonitoringStatisticsProcessor(injectionManager, this);
                this.monitoringStatisticsProcessor.startMonitoringWorker();
                break;
            case DESTROY_FINISHED:
                if (monitoringStatisticsProcessor != null) {
                    try {
                        monitoringStatisticsProcessor.shutDown();
                    } catch (final InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new ProcessingException(LocalizationMessages.ERROR_MONITORING_SHUTDOWN_INTERRUPTED(), e);
                    }
                }

                // onDestroy
                final List<DestroyListener> listeners =
                        injectionManager.getAllInstances(DestroyListener.class);

                for (final DestroyListener listener : listeners) {
                    try {
                        listener.onDestroy();
                    } catch (final Exception e) {
                        LOGGER.log(Level.WARNING,
                                LocalizationMessages.ERROR_MONITORING_STATISTICS_LISTENER_DESTROY(listener.getClass()), e);
                    }
                }

                break;

        }
    }

    private class ReqEventListener implements RequestEventListener {

        private final long requestTimeStart;
        private volatile long methodTimeStart;
        private volatile MethodStats methodStats;

        public ReqEventListener() {
            this.requestTimeStart = System.currentTimeMillis();
        }

        @Override
        public void onEvent(final RequestEvent event) {
            final long now = System.currentTimeMillis();

            switch (event.getType()) {
                case RESOURCE_METHOD_START:
                    this.methodTimeStart = now;
                    break;
                case RESOURCE_METHOD_FINISHED:
                    final ResourceMethod method = event.getUriInfo().getMatchedResourceMethod();
                    methodStats = new MethodStats(method, methodTimeStart, now - methodTimeStart);
                    break;
                case EXCEPTION_MAPPING_FINISHED:
                    if (!exceptionMapperEvents.offer(event)) {
                        LOGGER.warning(LocalizationMessages.ERROR_MONITORING_QUEUE_MAPPER());
                    }
                    break;
                case FINISHED:
                    if (event.isResponseWritten()) {
                        if (!responseStatuses.offer(event.getContainerResponse().getStatus())) {
                            LOGGER.warning(LocalizationMessages.ERROR_MONITORING_QUEUE_RESPONSE());
                        }
                    }
                    final StringBuilder sb = new StringBuilder();
                    final List<UriTemplate> orderedTemplates =
                            event.getUriInfo().getMatchedTemplates()
                                 .stream()
                                 .collect(Collectors.collectingAndThen(Collectors.toList(), uriTemplates -> {
                                     Collections.reverse(uriTemplates);
                                     return uriTemplates;
                                 }));

                    for (final UriTemplate uriTemplate : orderedTemplates) {
                        sb.append(uriTemplate.getTemplate());
                        if (!uriTemplate.endsWithSlash()) {
                            sb.append("/");
                        }
                        sb.setLength(sb.length() - 1);
                    }

                    if (!requestQueuedItems.offer(new RequestStats(new TimeStats(requestTimeStart, now - requestTimeStart),
                            methodStats, sb.toString()))) {
                        LOGGER.warning(LocalizationMessages.ERROR_MONITORING_QUEUE_REQUEST());
                    }

            }
        }
    }

    /**
     * Get the exception mapper event queue.
     *
     * @return Exception mapper event queue.
     */
    Queue<RequestEvent> getExceptionMapperEvents() {
        return exceptionMapperEvents;
    }

    /**
     * Get the request event queue.
     *
     * @return Request event queue.
     */
    Queue<RequestStats> getRequestQueuedItems() {
        return requestQueuedItems;
    }

    /**
     * Get the queue with response status codes.
     *
     * @return response status queue.
     */
    Queue<Integer> getResponseStatuses() {
        return responseStatuses;
    }
}
