/*
 * Jopr 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, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * 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 and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.rhq.plugins.jbossas;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;

import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.measurement.calltime.CallTimeData;
import org.rhq.core.domain.measurement.calltime.CallTimeDataValue;
import org.rhq.plugins.jmx.MBeanResourceComponent;

/**
 * A plugin component for managing an EJB2 session bean.
 *
 * @author Greg Hinkle
 * @author Ian Springer
 * @author John Mazzitelli
 */
public class EJB2BeanComponent extends MBeanResourceComponent<JBossASServerComponent> {
    private final Log log = LogFactory.getLog(EJB2BeanComponent.class);

    private Map<Integer, CallTimeData> previousRawCallTimeDatas = new HashMap<Integer, CallTimeData>();

    @Override
    public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> schedules) {
        Set<MeasurementScheduleRequest> numericMetricSchedules = new LinkedHashSet<MeasurementScheduleRequest>();
        for (MeasurementScheduleRequest schedule : schedules) {
            if (schedule.getDataType() == DataType.MEASUREMENT) {
                numericMetricSchedules.add(schedule);
            } else if (schedule.getName().equals("MethodInvocationTime")) {
                Object statelessSessionBeanStats; // javax.management.j2ee.statistics.StatelessSessionBeanStats
                try {
                    statelessSessionBeanStats = getStatelessSessionBeanStats();
                } catch (Exception e) {
                    // Unsure if this will ever happen - maybe the JBoss instance doesn't provide this operation?
                    continue;
                }

                try {
                    Map<String, Stat> stats = getStats(statelessSessionBeanStats);
                    Date lastResetTime = getLastResetTime(statelessSessionBeanStats);
                    Date now = new Date(System.currentTimeMillis());
                    if (!stats.isEmpty()) {
                        CallTimeData callTimeData = createCallTimeData(schedule, stats, lastResetTime, now);
                        report.addData(callTimeData);
                    }
                } catch (Exception e) {
                    log.error("Failed to retrieve EJB2 call-time data.", e);
                }
            }
        }

        super.getValues(report, numericMetricSchedules);
    }

    private CallTimeData createCallTimeData(MeasurementScheduleRequest schedule, Map<String, Stat> stats,
        Date lastResetTime, Date collectionTime) throws Exception {

        CallTimeData previousRawCallTimeData = this.previousRawCallTimeDatas.get(schedule.getScheduleId());
        CallTimeData rawCallTimeData = new CallTimeData(schedule);
        this.previousRawCallTimeDatas.put(schedule.getScheduleId(), rawCallTimeData);

        CallTimeData callTimeData = new CallTimeData(schedule);
        for (String methodName : stats.keySet()) {
            Stat timeStatistic = stats.get(methodName);
            long minTime = timeStatistic.min;
            long maxTime = timeStatistic.max;
            long totalTime = timeStatistic.total;
            long count = timeStatistic.count;
            if (count == 0) {
                // Don't bother even adding data for this method if the call count is 0.
                continue;
            }

            rawCallTimeData.addAggregatedCallData(methodName, lastResetTime, collectionTime, minTime, maxTime,
                totalTime, count);

            // Now compute the adjusted data, which is what we will report back to the server.
            CallTimeDataValue previousValue = (previousRawCallTimeData != null) ? previousRawCallTimeData.getValues()
                .get(methodName) : null;
            boolean supercedesPrevious = ((previousValue != null) && (previousValue.getBeginTime() == lastResetTime
                .getTime()));
            Date beginTime = lastResetTime;
            if (supercedesPrevious) {
                // The data for this method hasn't been reset since the last time we collected it.
                long countSincePrevious = count - previousValue.getCount();
                if (countSincePrevious > 0) {
                    // There have been new calls since the last time we collected data
                    // for this method. Adjust the time span to begin at the end of the
                    // time span from the previous collection.
                    beginTime = new Date(previousValue.getEndTime());

                    // Adjust the total and count to reflect the adjusted time span;
                    // do so by subtracting the previous values from the current values.
                    // NOTE: It isn't possible to figure out the minimum and maximum for
                    // the adjusted time span, so just leave them be. If they happen
                    // to have changed since the previous collection, they will be
                    // accurate; otherwise they will not.
                    count = countSincePrevious;
                    totalTime = totalTime - (long) previousValue.getTotal();
                }
                // else, the count hasn't changed, so don't bother adjusting the data;
                // when the server sees the data has the same begin time as
                // previously persisted data, it will replace the previous data with the
                // updated data (which will basically have a later end time)
            }

            callTimeData.addAggregatedCallData(methodName, beginTime, collectionTime, minTime, maxTime, totalTime,
                count);
        }

        return callTimeData;
    }

    private Date getLastResetTime(Object statelessSessionBeanStats) throws Exception {
        // XXX we assume no one will ever execute the Jboss MBean's resetStats
        // XXX we look at a count stat's start time and use that as the last reset time
        Method method = statelessSessionBeanStats.getClass().getMethod("getMethodReadyCount");
        Object methodReadyCountStat = method.invoke(statelessSessionBeanStats);
        long time = (Long) methodReadyCountStat.getClass().getMethod("getStartTime").invoke(methodReadyCountStat);
        return new Date(time);
    }

    private Object getStatelessSessionBeanStats() throws Exception {
        // will actually be a type of: javax.management.j2ee.statistics.StatelessSessionBeanStats
        Object statelessSessionBeanStats = null;

        // accesses the remote MBean to get information on the EJB
        EmsBean emsBean = getEmsBean();

        // use the connection's classloader so we use the appropriate class definitions
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(emsBean.getClass().getClassLoader());

        try {
            EmsAttribute attribute = emsBean.getAttribute("stats");
            statelessSessionBeanStats = attribute.refresh();
        } catch (RuntimeException e) {
            String msg = "Failed to retrieve EJB2 invocation stats.";
            if (log.isDebugEnabled()) {
                log.debug(msg, e);
            } else {
                log.info(msg + " Enable DEBUG logging to see cause.");
            }
            throw new Exception(msg, e);
        } finally {
            Thread.currentThread().setContextClassLoader(cl);
        }
        return statelessSessionBeanStats;
    }

    private Map<String, Stat> getStats(Object statelessSessionBeanStats) throws Exception {

        Map<String, Stat> stats = new HashMap<String, Stat>();

        Method method = statelessSessionBeanStats.getClass().getMethod("getStatistics");
        Object[] jbossStats = (Object[]) method.invoke(statelessSessionBeanStats); // javax.management.j2ee.statistics.Statistic[]

        if (jbossStats != null) {
            for (Object jbossStat : jbossStats) {
                Class<? extends Object> clazz = jbossStat.getClass();
                if (clazz.getSimpleName().contains("TimeStatistic")) {
                    // there appears to be a bug in Jboss stats - startTime always changes when you get
                    // the values and lastSampleTime is always 0.
                    Stat newStat = new Stat();
                    newStat.name = (String) clazz.getMethod("getName").invoke(jbossStat);
                    newStat.count = (Long) clazz.getMethod("getCount").invoke(jbossStat);
                    newStat.min = (Long) clazz.getMethod("getMinTime").invoke(jbossStat);
                    newStat.max = (Long) clazz.getMethod("getMaxTime").invoke(jbossStat);
                    newStat.total = (Long) clazz.getMethod("getTotalTime").invoke(jbossStat);
                    newStat.startTime = (Long) clazz.getMethod("getStartTime").invoke(jbossStat);
                    newStat.lastSampleTime = (Long) clazz.getMethod("getLastSampleTime").invoke(jbossStat);
                    stats.put(newStat.name, newStat);
                }
            }
        }

        return stats;
    }

    class Stat {
        String name;
        long count;
        long min;
        long max;
        long total;
        long startTime;
        long lastSampleTime;
    }
}