/*
 * 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.jackrabbit.oak.cache;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.concurrent.TimeUnit;

import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
import org.apache.jackrabbit.oak.api.jmx.ConsolidatedCacheStatsMBean;
import org.apache.jackrabbit.oak.api.jmx.PersistentCacheStatsMBean;
import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
import org.apache.jackrabbit.oak.spi.whiteboard.Tracker;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.osgi.framework.BundleContext;

import static org.apache.jackrabbit.oak.cache.CacheStats.timeInWords;
import static org.apache.jackrabbit.oak.commons.IOUtils.humanReadableByteCount;
import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;

@Component
public class ConsolidatedCacheStats implements ConsolidatedCacheStatsMBean {

    private Tracker<CacheStatsMBean> cacheStats;
    private Tracker<PersistentCacheStatsMBean> persistentCacheStats;

    private Registration mbeanReg;

    @Override
    public TabularData getCacheStats() {
        TabularDataSupport tds;
        try {
            TabularType tt = new TabularType(CacheStatsData.class.getName(),
                    "Consolidated Cache Stats", CacheStatsData.TYPE, new String[]{"name"});
            tds = new TabularDataSupport(tt);
            for(CacheStatsMBean stats : cacheStats.getServices()){
                tds.put(new CacheStatsData(stats).toCompositeData());
            }
            for(CacheStatsMBean stats : persistentCacheStats.getServices()){
                tds.put(new CacheStatsData(stats).toCompositeData());
            }
        } catch (OpenDataException e) {
            throw new IllegalStateException(e);
        }
        return tds;
    }

    @Activate
    private void activate(BundleContext context){
        Whiteboard wb = new OsgiWhiteboard(context);
        cacheStats = wb.track(CacheStatsMBean.class);
        persistentCacheStats = wb.track(PersistentCacheStatsMBean.class);
        mbeanReg = registerMBean(wb,
                ConsolidatedCacheStatsMBean.class,
                this,
                ConsolidatedCacheStatsMBean.TYPE,
                "Consolidated Cache statistics");
    }

    @Deactivate
    private void deactivate(){
        if(mbeanReg != null){
            mbeanReg.unregister();
        }

        if(cacheStats != null){
            cacheStats.stop();
        }

        if(persistentCacheStats != null) {
            persistentCacheStats.stop();
        }
    }

    private static class CacheStatsData {
        static final String[] FIELD_NAMES = new String[]{
                "name",
                "requestCount",
                "hitCount",
                "hitRate",
                "missCount",
                "missRate",
                "loadCount",
                "loadSuccessCount",
                "loadExceptionCount",
                "totalLoadTime",
                "averageLoadPenalty",
                "evictionCount",
                "elementCount",
                "totalWeight",
                "maxWeight",
        };

        static final String[] FIELD_DESCRIPTIONS = FIELD_NAMES;

        @SuppressWarnings("rawtypes")
        static final OpenType[] FIELD_TYPES = new OpenType[]{
                SimpleType.STRING,
                SimpleType.LONG,
                SimpleType.LONG,
                SimpleType.BIGDECIMAL,
                SimpleType.LONG,
                SimpleType.BIGDECIMAL,
                SimpleType.LONG,
                SimpleType.LONG,
                SimpleType.LONG,
                SimpleType.STRING,
                SimpleType.STRING,
                SimpleType.LONG,
                SimpleType.LONG,
                SimpleType.STRING,
                SimpleType.STRING,
        };

        static final CompositeType TYPE = createCompositeType();

        static CompositeType createCompositeType() {
            try {
                return new CompositeType(
                        CacheStatsData.class.getName(),
                        "Composite data type for Cache statistics",
                        CacheStatsData.FIELD_NAMES,
                        CacheStatsData.FIELD_DESCRIPTIONS,
                        CacheStatsData.FIELD_TYPES);
            } catch (OpenDataException e) {
                throw new IllegalStateException(e);
            }
        }

        private final CacheStatsMBean stats;

        public CacheStatsData(CacheStatsMBean stats){
            this.stats = stats;
        }

        CompositeDataSupport toCompositeData() {
            Object[] values = new Object[]{
                    stats.getName(),
                    stats.getRequestCount(),
                    stats.getHitCount(),
                    new BigDecimal(stats.getHitRate(),new MathContext(2)),
                    stats.getMissCount(),
                    new BigDecimal(stats.getMissRate(), new MathContext(2)),
                    stats.getLoadCount(),
                    stats.getLoadSuccessCount(),
                    stats.getLoadExceptionCount(),
                    timeInWords(stats.getTotalLoadTime()),
                    TimeUnit.NANOSECONDS.toMillis((long) stats.getAverageLoadPenalty()) + "ms",
                    stats.getEvictionCount(),
                    stats.getElementCount(),
                    humanReadableByteCount(stats.estimateCurrentWeight()),
                    humanReadableByteCount(stats.getMaxTotalWeight()),
            };
            try {
                return new CompositeDataSupport(TYPE, FIELD_NAMES, values);
            } catch (OpenDataException e) {
                throw new IllegalStateException(e);
            }
        }
    }

}
