/*
 * 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.plugins.metric;

import java.io.Closeable;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.ObjectNameFactory;
import com.codahale.metrics.RatioGauge;
import com.codahale.metrics.Timer;
import org.apache.jackrabbit.guava.common.collect.Maps;
import org.apache.jackrabbit.api.stats.RepositoryStatistics;
import org.apache.jackrabbit.api.stats.RepositoryStatistics.Type;
import org.apache.jackrabbit.oak.commons.jmx.JmxUtil;
import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
import org.apache.jackrabbit.oak.stats.Clock;
import org.apache.jackrabbit.oak.stats.CounterStats;
import org.apache.jackrabbit.oak.stats.HistogramStats;
import org.apache.jackrabbit.oak.stats.MeterStats;
import org.apache.jackrabbit.oak.stats.SimpleStats;
import org.apache.jackrabbit.oak.stats.StatisticsProvider;
import org.apache.jackrabbit.oak.stats.Stats;
import org.apache.jackrabbit.oak.stats.StatsOptions;
import org.apache.jackrabbit.oak.stats.TimerStats;
import org.apache.jackrabbit.stats.RepositoryStatisticsImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MetricStatisticsProvider implements StatisticsProvider, Closeable {
    private static final Logger log = LoggerFactory.getLogger(MetricStatisticsProvider.class);

    private static final String JMX_TYPE_METRICS = "Metrics";

    private final ConcurrentMap<String, Stats> statsRegistry = Maps.newConcurrentMap();
    private final MetricRegistry registry;
    private final JmxReporter reporter;
    private final RepositoryStatisticsImpl repoStats;
    private final Clock.Fast clock;
    private final com.codahale.metrics.Clock metricsClock;

    public MetricStatisticsProvider(MBeanServer server, ScheduledExecutorService executor) {
        clock = new Clock.Fast(executor);
        metricsClock = new OakMetricClock(clock);
        registry = new MetricRegistry();
        repoStats = new RepositoryStatisticsImpl(executor);
        reporter = JmxReporter.forRegistry(registry)
                .inDomain(WhiteboardUtils.JMX_OAK_DOMAIN)
                .registerWith(server)
                .createsObjectNamesWith(new OakNameFactory())
                .build();
        reporter.start();

        registerAverages();
    }

    static String typeToName(Type type) {
        return type.name();
    }

    public void close() {
        clock.close();
        if (reporter != null) {
            reporter.close();
        }
    }

    @Override
    public RepositoryStatistics getStats() {
        return repoStats;
    }

    @Override
    public MeterStats getMeter(String name, StatsOptions options) {
        return getStats(name, StatsBuilder.METERS, options);
    }

    @Override
    public CounterStats getCounterStats(String name, StatsOptions options) {
        return getStats(name, StatsBuilder.COUNTERS, options);
    }

    @Override
    public TimerStats getTimer(String name, StatsOptions options) {
        return getStats(name, StatsBuilder.TIMERS, options);
    }

    @Override
    public HistogramStats getHistogram(String name, StatsOptions options) {
        return getStats(name, StatsBuilder.HISTOGRAMS, options);
    }

    public MetricRegistry getRegistry() {
        return registry;
    }

    RepositoryStatisticsImpl getRepoStats() {
        return repoStats;
    }

    private <T extends Stats> T getStats(String name, StatsBuilder<T> builder, StatsOptions options) {
        Stats stats = statsRegistry.get(name);
        //Use double locking pattern. The map should get populated with required set
        //during startup phase so for later calls should not hit the synchronized block
        if (stats == null) {
            synchronized (this){
                stats = statsRegistry.get(name);
                //If still null then go ahead and create it within lock
                if (stats == null){
                    if (options.isOnlyMetricEnabled()) {
                        stats = builder.newMetric(this, name);
                    } else if (options.isOnlyTimeSeriesEnabled()) {
                        stats = new SimpleStats(getTimerSeriesStats(name, builder), builder.getType());
                    } else {
                        stats = builder.newComposite(getTimerSeriesStats(name, builder), this, name);
                    }
                    statsRegistry.put(name, stats);
                }
            }
        }

        if (builder.isInstance(stats)) {
            //noinspection unchecked
            return (T) stats;
        }

        throw new IllegalStateException();
    }

    private AtomicLong getTimerSeriesStats(String name, StatsBuilder builder){
        AtomicLong counter;
        Type enumType = Type.getType(name);
        if (enumType != null) {
            counter = repoStats.getCounter(enumType);
        } else {
            boolean resetValueEachSecond = builder != StatsBuilder.COUNTERS;
            counter = repoStats.getCounter(name, resetValueEachSecond);
        }
        return counter;
    }

    private void registerAverages() {
        registry.register(typeToName(Type.OBSERVATION_EVENT_AVERAGE),
                new AvgGauge(compStats(Type.OBSERVATION_EVENT_COUNTER, StatsBuilder.METERS).getMeter(),
                        compStats(Type.OBSERVATION_EVENT_DURATION, StatsBuilder.TIMERS).getTimer()));
    }

    private CompositeStats compStats(Type type, StatsBuilder builder){
        Stats stats = getStats(typeToName(type), builder, StatsOptions.DEFAULT);
        return (CompositeStats) stats;
    }

    @SuppressWarnings("unused")
    private interface StatsBuilder<T extends Stats> {
        StatsBuilder<CounterStats> COUNTERS = new StatsBuilder<CounterStats>() {
            @Override
            public CompositeStats newComposite(AtomicLong delegate, MetricStatisticsProvider provider, String name) {
                return new CompositeStats(delegate, provider.registry.counter(name));
            }

            @Override
            public Stats newMetric(MetricStatisticsProvider provider, String name) {
                return new CounterImpl(provider.registry.counter(name));
            }

            @Override
            public boolean isInstance(Stats metric) {
                return CounterStats.class.isInstance(metric);
            }

            @Override
            public SimpleStats.Type getType() {
                return SimpleStats.Type.COUNTER;
            }
        };

        StatsBuilder<MeterStats> METERS = new StatsBuilder<MeterStats>() {
            @Override
            public CompositeStats newComposite(AtomicLong delegate, MetricStatisticsProvider provider, String name) {
                return new CompositeStats(delegate, getMeter(provider, name));
            }

            @Override
            public Stats newMetric(MetricStatisticsProvider provider, String name) {
                return new MeterImpl(getMeter(provider, name));
            }

            @Override
            public boolean isInstance(Stats metric) {
                return MeterStats.class.isInstance(metric);
            }

            @Override
            public SimpleStats.Type getType() {
                return SimpleStats.Type.METER;
            }

            private Meter getMeter(MetricStatisticsProvider provider, String name) {
                Meter meter = new Meter(provider.metricsClock);
                provider.registry.register(name, meter);
                return meter;
            }
        };

        StatsBuilder<TimerStats> TIMERS = new StatsBuilder<TimerStats>() {

            @Override
            public CompositeStats newComposite(AtomicLong delegate, MetricStatisticsProvider provider, String name) {
                return new CompositeStats(delegate, provider.registry.timer(name));
            }

            @Override
            public Stats newMetric(MetricStatisticsProvider provider, String name) {
                return new TimerImpl(provider.registry.timer(name));
            }

            @Override
            public boolean isInstance(Stats metric) {
                return TimerStats.class.isInstance(metric);
            }

            @Override
            public SimpleStats.Type getType() {
                return SimpleStats.Type.TIMER;
            }
        };

        StatsBuilder<HistogramStats> HISTOGRAMS = new StatsBuilder<HistogramStats>() {

            @Override
            public CompositeStats newComposite(AtomicLong delegate, MetricStatisticsProvider provider, String name) {
                return new CompositeStats(delegate, provider.registry.histogram(name));
            }

            @Override
            public Stats newMetric(MetricStatisticsProvider provider, String name) {
                return new HistogramImpl(provider.registry.histogram(name));
            }

            @Override
            public boolean isInstance(Stats metric) {
                return HistogramStats.class.isInstance(metric);
            }

            @Override
            public SimpleStats.Type getType() {
                return SimpleStats.Type.HISTOGRAM;
            }
        };

        CompositeStats newComposite(AtomicLong delegate, MetricStatisticsProvider provider, String name);

        Stats newMetric(MetricStatisticsProvider provider,String name);

        boolean isInstance(Stats stats);

        SimpleStats.Type getType();
    }

    private static class AvgGauge extends RatioGauge {
        private final Meter meter;
        private final Timer timer;

        private AvgGauge(Meter meter, Timer timer) {
            this.meter = meter;
            this.timer = timer;
        }

        @Override
        protected Ratio getRatio() {
            //TODO Should we use getMeanRate
            return Ratio.of(meter.getFifteenMinuteRate(),
                    timer.getFifteenMinuteRate());
        }
    }

    private static class OakNameFactory implements ObjectNameFactory {
        @Override
        public ObjectName createName(String type, String domain, String name) {
            Hashtable<String, String> table = new Hashtable<String, String>();
            table.put("type", JMX_TYPE_METRICS);
            table.put("name", JmxUtil.quoteValueIfRequired(name));
            try {
                return new ObjectName(domain, table);
            } catch (MalformedObjectNameException e) {
                log.warn("Unable to register {} {}", type, name, e);
                throw new RuntimeException(e);
            }
        }
    }

    private static class OakMetricClock extends com.codahale.metrics.Clock {
        private final Clock clock;

        public OakMetricClock(Clock clock) {
            this.clock = clock;
        }

        @Override
        public long getTick() {
            return TimeUnit.NANOSECONDS.convert(clock.getTime(), TimeUnit.MILLISECONDS);
        }
    }


}
