/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package org.rhq.enterprise.server.remote;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Provides metrics for remote invocations from things like the remote CLI.
 * 
 * @author John Mazzitelli
 */
public class RemoteSafeInvocationHandlerMetrics implements RemoteSafeInvocationHandlerMetricsMBean {

    private long numberSuccessfulInvocations = 0L;
    private long numberFailedInvocations = 0L;
    private long averageExecutionTime = 0L;

    private Map<String, Calltime> calltimes = new HashMap<String, Calltime>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void clear() {
        writeLock();
        try {
            numberSuccessfulInvocations = 0L;
            numberFailedInvocations = 0L;
            averageExecutionTime = 0L;
            calltimes.clear();
        } finally {
            writeUnlock();
        }
    }

    public Map<String, Calltime> getCallTimeData() {
        // just do a shallow copy - this allows us to avoid exceptions when we want to concurrently
        // traverse the calltimes and add to them but it avoids unnecesary duplication of MinMaxAvg
        // objects (thus we are more efficient both in performance and space). Note that MinMaxAvg
        // is allowed to be concurrently accessed - even though its possible you could try to get
        // its data while we are writing new min/max/avg/count values. Even though the data won't be
        // entirely accurate and up-to-date in that case, its good enough for our purposes.
        readLock();
        try {
            return new HashMap<String, Calltime>(calltimes);
        } finally {
            readUnlock();
        }
    }

    public Map<String, long[]> getCallTimeDataAsPrimitives() {
        Map<String, Calltime> calltimeData = getCallTimeData();
        Map<String, long[]> calltimeDataPrimitives = new HashMap<String, long[]>(calltimeData.size());
        for (Map.Entry<String, Calltime> entry : calltimeData.entrySet()) {
            Calltime calltime = entry.getValue();
            calltimeDataPrimitives.put(entry.getKey(), new long[] { calltime.getCount(), calltime.getSuccesses(),
                calltime.getFailures(), calltime.getMinimum(), calltime.getMaximum(), calltime.getAverage() });
        }
        return calltimeDataPrimitives;
    }

    public long getNumberFailedInvocations() {
        readLock();
        try {
            return numberFailedInvocations;
        } finally {
            readUnlock();
        }
    }

    public long getNumberSuccessfulInvocations() {
        readLock();
        try {
            return numberSuccessfulInvocations;
        } finally {
            readUnlock();
        }
    }

    public long getNumberTotalInvocations() {
        readLock();
        try {
            return numberSuccessfulInvocations + numberFailedInvocations;
        } finally {
            readUnlock();
        }
    }

    public long getAverageExecutionTime() {
        readLock();
        try {
            return averageExecutionTime;
        } finally {
            readUnlock();
        }
    }

    /**
     * Add a newly collected metric value for a particular type of invocation to
     * the stored calltime data. This will update the min/max/avg data, but only
     * if this represents a succesful call (i.e. <code>successful</code> is <code>true</code>).
     * We do not want to skew the min/max/avg results from failed invocations because
     * they almost always fail-fast and will have very fast execution times.  
     * 
     * This is packaged-scoped because only the handler should be
     * adding calltime data to this object.
     *
     * @param type the type of invocation whose min/max/avg is to be stored
     * @param executionTime the time, in milliseconds, that the type invocation took
     * @param successful <code>true</code> if the invocation was successful
     */
    void addData(String type, long executionTime, boolean successful) {
        writeLock();
        try {
            if (type == null) {
                type = "(unknown)";
            }

            Calltime calltime = calltimes.get(type);
            if (calltime == null) {
                calltime = new Calltime();
                calltimes.put(type, calltime);
            }

            calltime.count++;
            if (successful) {
                this.numberSuccessfulInvocations++;
                this.averageExecutionTime = (((this.numberSuccessfulInvocations - 1) * this.averageExecutionTime) + executionTime)
                    / this.numberSuccessfulInvocations;

                if (executionTime > calltime.max) {
                    calltime.max = executionTime;
                }
                if (executionTime < calltime.min) {
                    calltime.min = executionTime;
                }
                long successes = calltime.getSuccesses();
                calltime.avg = (((successes - 1) * calltime.avg) + executionTime) / successes;
            } else {
                this.numberFailedInvocations++;
                calltime.failures++;
            }
        } finally {
            writeUnlock();
        }

        return;
    }

    private boolean writeLock() {
        // we try to be good stewards of this object by trying to be thread safe
        // but do not block the processor here for too long - we don't want to
        // prevent the processor from processing messages for too long just to
        // synchronize metric data. If we timeout or are interrupted, return immediately.
        // Note also that we don't want to throw any exceptions to the caller, try our
        // best to be fault tolerant in here.
        try {
            return lock.writeLock().tryLock(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // don't wait any longer, just return immediately
        } catch (Exception e) {
            lock = new ReentrantReadWriteLock(); // something really bad happened, let's create a new one to be safe
        }
        return false;
    }

    private void writeUnlock() {
        try {
            lock.writeLock().unlock();
        } catch (Exception e) {
            // Note that we don't want to throw any exceptions to the caller, try our
            // best to be fault tolerant in here. This exception occurred probably because
            // the caller didn't have the lock due to a timeout or interrupt in writeLock.
        }
    }

    private boolean readLock() {
        // we try to be good stewards of this object by trying to be thread safe
        // but if we can't get the lock, just return and let's read the data
        // unlocked.  Nothing dangerous will happen, at worst we might read
        // inconsistent metric data for this thread, nothing too serious to worry about.
        try {
            return lock.readLock().tryLock(30, TimeUnit.SECONDS);
        } catch (Exception e) {
        }
        return false;
    }

    private void readUnlock() {
        try {
            lock.readLock().unlock();
        } catch (Exception e) {
            // Note that we don't want to throw any exceptions to the caller, try our
            // best to be fault tolerant in here. This exception occurred probably because
            // the caller didn't have the lock due to a timeout or interrupt in readLock.
        }
    }

    /**
     * Used to store the minimum, maximum and average times (in milliseconds)
     * for invocations to a particular invocation. The count of the number
     * of times an invocation was executed is also kept.  Note that the min/max/avg
     * times will only be for successful invocations.
     */
    public class Calltime implements Serializable {
        private static final long serialVersionUID = 1L;

        private long count = 0;
        private long failures = 0;
        private long min = Long.MAX_VALUE;
        private long max = Long.MIN_VALUE;
        private long avg = 0;

        public long getCount() {
            return count;
        }

        public long getFailures() {
            return failures;
        }

        public long getSuccesses() {
            return count - (failures); // ok if not thread-safe, good enough for what we need
        }

        public long getMinimum() {
            return min;
        }

        public long getMaximum() {
            return max;
        }

        public long getAverage() {
            return avg;
        }

        @Override
        public String toString() {
            return "" + count + ':' + failures + ':' + min + ':' + max + ':' + avg;
        }
    }
}
