/*
 *  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.mina.filter.statistic;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.mina.core.filterchain.IoFilterAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoEventType;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteRequest;

/**
 * This class will measure the time it takes for a method in the {@link IoFilterAdapter} class to execute. The basic
 * premise of the logic in this class is to get the current time at the beginning of the method, call method on
 * nextFilter, and then get the current time again. An example of how to use the filter is:
 *
 * <pre>
 * ProfilerTimerFilter profiler = new ProfilerTimerFilter(TimeUnit.MILLISECOND, IoEventType.MESSAGE_RECEIVED);
 * chain.addFirst("Profiler", profiler);
 * </pre>
 * 
 * The profiled {@link IoEventType} are :
 * <ul>
 * <li>IoEventType.MESSAGE_RECEIVED</li>
 * <li>IoEventType.MESSAGE_SENT</li>
 * <li>IoEventType.SESSION_CREATED</li>
 * <li>IoEventType.SESSION_OPENED</li>
 * <li>IoEventType.SESSION_IDLE</li>
 * <li>IoEventType.SESSION_CLOSED</li>
 * </ul>
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 * @org.apache.xbean.XBean
 */
public class ProfilerTimerFilter extends IoFilterAdapter {
    /** TRhe selected time unit */
    private volatile TimeUnit timeUnit;

    /** A TimerWorker for the MessageReceived events */
    private TimerWorker messageReceivedTimerWorker;

    /** A flag to tell the filter that the MessageReceived must be profiled */
    private boolean profileMessageReceived = false;

    /** A TimerWorker for the MessageSent events */
    private TimerWorker messageSentTimerWorker;

    /** A flag to tell the filter that the MessageSent must be profiled */
    private boolean profileMessageSent = false;

    /** A TimerWorker for the SessionCreated events */
    private TimerWorker sessionCreatedTimerWorker;

    /** A flag to tell the filter that the SessionCreated must be profiled */
    private boolean profileSessionCreated = false;

    /** A TimerWorker for the SessionOpened events */
    private TimerWorker sessionOpenedTimerWorker;

    /** A flag to tell the filter that the SessionOpened must be profiled */
    private boolean profileSessionOpened = false;

    /** A TimerWorker for the SessionIdle events */
    private TimerWorker sessionIdleTimerWorker;

    /** A flag to tell the filter that the SessionIdle must be profiled */
    private boolean profileSessionIdle = false;

    /** A TimerWorker for the SessionClosed events */
    private TimerWorker sessionClosedTimerWorker;

    /** A flag to tell the filter that the SessionClosed must be profiled */
    private boolean profileSessionClosed = false;

    /**
     * Creates a new instance of ProfilerFilter. This is the default constructor and will print out timings for
     * messageReceived and messageSent and the time increment will be in milliseconds.
     */
    public ProfilerTimerFilter() {
        this(TimeUnit.MILLISECONDS, IoEventType.MESSAGE_RECEIVED, IoEventType.MESSAGE_SENT);
    }

    /**
     * Creates a new instance of ProfilerFilter. This is the default constructor and will print out timings for
     * messageReceived and messageSent.
     * 
     * @param timeUnit
     *            the time increment to set
     */
    public ProfilerTimerFilter(TimeUnit timeUnit) {
        this(timeUnit, IoEventType.MESSAGE_RECEIVED, IoEventType.MESSAGE_SENT);
    }

    /**
     * Creates a new instance of ProfilerFilter. An example of this call would be:
     *
     * <pre>
     * new ProfilerTimerFilter(TimeUnit.MILLISECONDS, IoEventType.MESSAGE_RECEIVED, IoEventType.MESSAGE_SENT);
     * </pre>
     * 
     * Note : you can add as many {@link IoEventType} as you want. The method accepts a variable number of arguments.
     * 
     * @param timeUnit
     *            Used to determine the level of precision you need in your timing.
     * @param eventTypes
     *            A list of {@link IoEventType} representation of the methods to profile
     */
    public ProfilerTimerFilter(TimeUnit timeUnit, IoEventType... eventTypes) {
        this.timeUnit = timeUnit;

        setProfilers(eventTypes);
    }

    /**
     * Create the profilers for a list of {@link IoEventType}.
     * 
     * @param eventTypes
     *            the list of {@link IoEventType} to profile
     */
    private void setProfilers(IoEventType... eventTypes) {
        for (IoEventType type : eventTypes) {
            switch (type) {
                case MESSAGE_RECEIVED:
                    messageReceivedTimerWorker = new TimerWorker();
                    profileMessageReceived = true;
                    break;

                case MESSAGE_SENT:
                    messageSentTimerWorker = new TimerWorker();
                    profileMessageSent = true;
                    break;

                case SESSION_CLOSED:
                    sessionClosedTimerWorker = new TimerWorker();
                    profileSessionClosed = true;
                    break;

                case SESSION_CREATED:
                    sessionCreatedTimerWorker = new TimerWorker();
                    profileSessionCreated = true;
                    break;

                case SESSION_IDLE:
                    sessionIdleTimerWorker = new TimerWorker();
                    profileSessionIdle = true;
                    break;

                case SESSION_OPENED:
                    sessionOpenedTimerWorker = new TimerWorker();
                    profileSessionOpened = true;
                    break;

                default:
                    break;
            }
        }
    }

    /**
     * Sets the {@link TimeUnit} being used.
     *
     * @param timeUnit
     *            the new {@link TimeUnit} to be used.
     */
    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }

    /**
     * Set the {@link IoEventType} to be profiled
     *
     * @param type
     *            The {@link IoEventType} to profile
     */
    public void profile(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                profileMessageReceived = true;

                if (messageReceivedTimerWorker == null) {
                    messageReceivedTimerWorker = new TimerWorker();
                }

                return;

            case MESSAGE_SENT:
                profileMessageSent = true;

                if (messageSentTimerWorker == null) {
                    messageSentTimerWorker = new TimerWorker();
                } 

                return;

            case SESSION_CLOSED:
                profileSessionClosed = true;

                if (sessionClosedTimerWorker == null) {
                    sessionClosedTimerWorker = new TimerWorker();
                }

                return;

            case SESSION_CREATED:
                profileSessionCreated = true;

                if (sessionCreatedTimerWorker == null) {
                    sessionCreatedTimerWorker = new TimerWorker();
                }

                return;

            case SESSION_IDLE:
                profileSessionIdle = true;

                if (sessionIdleTimerWorker == null) {
                    sessionIdleTimerWorker = new TimerWorker();
                }

                return;

            case SESSION_OPENED:
                profileSessionOpened = true;

                if (sessionOpenedTimerWorker == null) {
                    sessionOpenedTimerWorker = new TimerWorker();
                }

                return;

            default:
                break;
        }
    }

    /**
     * Stop profiling an {@link IoEventType}
     *
     * @param type
     *            The {@link IoEventType} to stop profiling
     */
    public void stopProfile(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                profileMessageReceived = false;
                return;

            case MESSAGE_SENT:
                profileMessageSent = false;
                return;

            case SESSION_CLOSED:
                profileSessionClosed = false;
                return;

            case SESSION_CREATED:
                profileSessionCreated = false;
                return;

            case SESSION_IDLE:
                profileSessionIdle = false;
                return;

            case SESSION_OPENED:
                profileSessionOpened = false;
                return;

            default:
                return;
        }
    }

    /**
     * Return the set of {@link IoEventType} which are profiled.
     *
     * @return a Set containing all the profiled {@link IoEventType}
     */
    public Set<IoEventType> getEventsToProfile() {
        Set<IoEventType> set = new HashSet<>();

        if (profileMessageReceived) {
            set.add(IoEventType.MESSAGE_RECEIVED);
        }

        if (profileMessageSent) {
            set.add(IoEventType.MESSAGE_SENT);
        }

        if (profileSessionCreated) {
            set.add(IoEventType.SESSION_CREATED);
        }

        if (profileSessionOpened) {
            set.add(IoEventType.SESSION_OPENED);
        }

        if (profileSessionIdle) {
            set.add(IoEventType.SESSION_IDLE);
        }

        if (profileSessionClosed) {
            set.add(IoEventType.SESSION_CLOSED);
        }

        return set;
    }

    /**
     * Set the profilers for a list of {@link IoEventType}
     * 
     * @param eventTypes
     *            the list of {@link IoEventType} to profile
     */
    public void setEventsToProfile(IoEventType... eventTypes) {
        setProfilers(eventTypes);
    }

    /**
     * Profile a MessageReceived event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     * @param message
     *            the received message
     */
    @Override
    public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
        if (profileMessageReceived) {
            long start = timeNow();
            nextFilter.messageReceived(session, message);
            long end = timeNow();
            messageReceivedTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.messageReceived(session, message);
        }
    }

    /**
     * Profile a MessageSent event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     * @param writeRequest
     *            the sent message
     */
    @Override
    public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
        if (profileMessageSent) {
            long start = timeNow();
            nextFilter.messageSent(session, writeRequest);
            long end = timeNow();
            messageSentTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.messageSent(session, writeRequest);
        }
    }

    /**
     * Profile a SessionCreated event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     */
    @Override
    public void sessionCreated(NextFilter nextFilter, IoSession session) throws Exception {
        if (profileSessionCreated) {
            long start = timeNow();
            nextFilter.sessionCreated(session);
            long end = timeNow();
            sessionCreatedTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.sessionCreated(session);
        }
    }

    /**
     * Profile a SessionOpened event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     */
    @Override
    public void sessionOpened(NextFilter nextFilter, IoSession session) throws Exception {
        if (profileSessionOpened) {
            long start = timeNow();
            nextFilter.sessionOpened(session);
            long end = timeNow();
            sessionOpenedTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.sessionOpened(session);
        }
    }

    /**
     * Profile a SessionIdle event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     * @param status
     *            The session's status
     */
    @Override
    public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
        if (profileSessionIdle) {
            long start = timeNow();
            nextFilter.sessionIdle(session, status);
            long end = timeNow();
            sessionIdleTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.sessionIdle(session, status);
        }
    }

    /**
     * Profile a SessionClosed event. This method will gather the following informations : - the method duration - the
     * shortest execution time - the slowest execution time - the average execution time - the global number of calls
     * 
     * @param nextFilter
     *            The filter to call next
     * @param session
     *            The associated session
     */
    @Override
    public void sessionClosed(NextFilter nextFilter, IoSession session) throws Exception {
        if (profileSessionClosed) {
            long start = timeNow();
            nextFilter.sessionClosed(session);
            long end = timeNow();
            sessionClosedTimerWorker.addNewDuration(end - start);
        } else {
            nextFilter.sessionClosed(session);
        }
    }

    /**
     * Get the average time for the specified method represented by the {@link IoEventType}
     *
     * @param type
     *            The {@link IoEventType} that the user wants to get the average method call time
     * @return The average time it took to execute the method represented by the {@link IoEventType}
     */
    public double getAverageTime(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                if (profileMessageReceived) {
                   return messageReceivedTimerWorker.getAverage();
                }

                break;

            case MESSAGE_SENT:
                if (profileMessageSent) {
                    return messageSentTimerWorker.getAverage();
                }

                break;

            case SESSION_CLOSED:
                if (profileSessionClosed) {
                    return sessionClosedTimerWorker.getAverage();
                }

                break;

            case SESSION_CREATED:
                if (profileSessionCreated) {
                    return sessionCreatedTimerWorker.getAverage();
                }

                break;

            case SESSION_IDLE:
                if (profileSessionIdle) {
                    return sessionIdleTimerWorker.getAverage();
                }

                break;

            case SESSION_OPENED:
                if (profileSessionOpened) {
                    return sessionOpenedTimerWorker.getAverage();
                }

                break;

            default:
                break;
        }

        throw new IllegalArgumentException("You are not monitoring this event.  Please add this event first.");
    }

    /**
     * Gets the total number of times the method has been called that is represented by the {@link IoEventType}
     *
     * @param type
     *            The {@link IoEventType} that the user wants to get the total number of method calls
     * @return The total number of method calls for the method represented by the {@link IoEventType}
     */
    public long getTotalCalls(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                if (profileMessageReceived) {
                    return messageReceivedTimerWorker.getCallsNumber();
                }

                break;

            case MESSAGE_SENT:
                if (profileMessageSent) {
                    return messageSentTimerWorker.getCallsNumber();
                }

                break;

            case SESSION_CLOSED:
                if (profileSessionClosed) {
                    return sessionClosedTimerWorker.getCallsNumber();
                }

                break;

            case SESSION_CREATED:
                if (profileSessionCreated) {
                    return sessionCreatedTimerWorker.getCallsNumber();
                }

                break;

            case SESSION_IDLE:
                if (profileSessionIdle) {
                    return sessionIdleTimerWorker.getCallsNumber();
                }

                break;

            case SESSION_OPENED:
                if (profileSessionOpened) {
                    return sessionOpenedTimerWorker.getCallsNumber();
                }

                break;

            default:
                break;
        }

        throw new IllegalArgumentException("You are not monitoring this event.  Please add this event first.");
    }

    /**
     * The total time this method has been executing
     *
     * @param type
     *            The {@link IoEventType} that the user wants to get the total time this method has been executing
     * @return The total time for the method represented by the {@link IoEventType}
     */
    public long getTotalTime(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                if (profileMessageReceived) {
                    return messageReceivedTimerWorker.getTotal();
                }

                break;

            case MESSAGE_SENT:
                if (profileMessageSent) {
                    return messageSentTimerWorker.getTotal();
                }

                break;

            case SESSION_CLOSED:
                if (profileSessionClosed) {
                    return sessionClosedTimerWorker.getTotal();
                }

                break;

            case SESSION_CREATED:
                if (profileSessionCreated) {
                    return sessionCreatedTimerWorker.getTotal();
                }

                break;

            case SESSION_IDLE:
                if (profileSessionIdle) {
                    return sessionIdleTimerWorker.getTotal();
                }

                break;

            case SESSION_OPENED:
                if (profileSessionOpened) {
                    return sessionOpenedTimerWorker.getTotal();
                }

                break;

            default:
                break;
        }

        throw new IllegalArgumentException("You are not monitoring this event.  Please add this event first.");
    }

    /**
     * The minimum time the method represented by {@link IoEventType} has executed
     *
     * @param type
     *            The {@link IoEventType} that the user wants to get the minimum time this method has executed
     * @return The minimum time this method has executed represented by the {@link IoEventType}
     */
    public long getMinimumTime(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                if (profileMessageReceived) {
                    return messageReceivedTimerWorker.getMinimum();
                }

                break;

            case MESSAGE_SENT:
                if (profileMessageSent) {
                    return messageSentTimerWorker.getMinimum();
                }

                break;

            case SESSION_CLOSED:
                if (profileSessionClosed) {
                    return sessionClosedTimerWorker.getMinimum();
                }

                break;

            case SESSION_CREATED:
                if (profileSessionCreated) {
                    return sessionCreatedTimerWorker.getMinimum();
                }

                break;

            case SESSION_IDLE:
                if (profileSessionIdle) {
                    return sessionIdleTimerWorker.getMinimum();
                }

                break;

            case SESSION_OPENED:
                if (profileSessionOpened) {
                    return sessionOpenedTimerWorker.getMinimum();
                }

                break;

            default:
                break;
        }

        throw new IllegalArgumentException("You are not monitoring this event.  Please add this event first.");
    }

    /**
     * The maximum time the method represented by {@link IoEventType} has executed
     *
     * @param type
     *            The {@link IoEventType} that the user wants to get the maximum time this method has executed
     * @return The maximum time this method has executed represented by the {@link IoEventType}
     */
    public long getMaximumTime(IoEventType type) {
        switch (type) {
            case MESSAGE_RECEIVED:
                if (profileMessageReceived) {
                    return messageReceivedTimerWorker.getMaximum();
                }

                break;

            case MESSAGE_SENT:
                if (profileMessageSent) {
                    return messageSentTimerWorker.getMaximum();
                }

                break;

            case SESSION_CLOSED:
                if (profileSessionClosed) {
                    return sessionClosedTimerWorker.getMaximum();
                }

                break;

            case SESSION_CREATED:
                if (profileSessionCreated) {
                    return sessionCreatedTimerWorker.getMaximum();
                }

                break;

            case SESSION_IDLE:
                if (profileSessionIdle) {
                    return sessionIdleTimerWorker.getMaximum();
                }

                break;

            case SESSION_OPENED:
                if (profileSessionOpened) {
                    return sessionOpenedTimerWorker.getMaximum();
                }

                break;

            default:
                break;
        }

        throw new IllegalArgumentException("You are not monitoring this event.  Please add this event first.");
    }

    /**
     * Class that will track the time each method takes and be able to provide information for each method.
     *
     */
    private class TimerWorker {
        /** The sum of all operation durations */
        private final AtomicLong total;

        /** The number of calls */
        private final AtomicLong callsNumber;

        /** The fastest operation */
        private final AtomicLong minimum;

        /** The slowest operation */
        private final AtomicLong maximum;

        /** A lock for synchinized blocks */
        private final Object lock = new Object();

        /**
         * Creates a new instance of TimerWorker.
         *
         */
        public TimerWorker() {
            total = new AtomicLong();
            callsNumber = new AtomicLong();
            minimum = new AtomicLong();
            maximum = new AtomicLong();
        }

        /**
         * Add a new operation duration to this class. Total is updated and calls is incremented
         *
         * @param duration
         *            The new operation duration
         */
        public void addNewDuration(long duration) {
            callsNumber.incrementAndGet();
            total.addAndGet(duration);

            synchronized (lock) {
                // this is not entirely thread-safe, must lock
                if (duration < minimum.longValue()) {
                    minimum.set(duration);
                }

                // this is not entirely thread-safe, must lock
                if (duration > maximum.longValue()) {
                    maximum.set(duration);
                }
            }
        }

        /**
         * Gets the average reading for this event
         *
         * @return the average reading for this event
         */
        public double getAverage() {
            synchronized (lock) {
                // There are two operations, we need to synchronize the block
                return callsNumber.longValue() != 0 ? total.longValue() / callsNumber.longValue() : 0;
            }
        }

        /**
         * @return The total number of profiled operation
         */
        public long getCallsNumber() {
            return callsNumber.longValue();
        }

        /**
         * @return the total time
         */
        public long getTotal() {
            return total.longValue();
        }

        /**
         * @return the lowest execution time
         */
        public long getMinimum() {
            return minimum.longValue();
        }

        /**
         * @return the longest execution time
         */
        public long getMaximum() {
            return maximum.longValue();
        }
    }

    /**
     * @return the current time, expressed using the fixed TimeUnit.
     */
    private long timeNow() {
        switch (timeUnit) {
            case SECONDS:
                return System.currentTimeMillis() / 1000;

            case MICROSECONDS:
                return System.nanoTime() / 1000;

            case NANOSECONDS:
                return System.nanoTime();

            default:
                return System.currentTimeMillis();
        }
    }
}
