/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project 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:
 *
 *   https://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 io.netty.test.udt.util;

import com.google.caliper.Measurement;
import com.google.caliper.MeasurementSet;
import com.google.caliper.Run;
import com.google.caliper.Scenario;
import com.google.caliper.ScenarioResult;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Timer;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Caliper measure with Metrics provider.
 * <p>
 * measure up to 3 values: {@link #rate()}, {@link #time()}, {@link #size()}
 */
public class CaliperMeasure {

    /**
     * Gauge any double value
     */
    public static class SizeGuage extends Gauge<Double> {

        private volatile Double size = 0.0;

        @Override
        public Double value() {
            return size;
        }

        public void value(final double size) {
            this.size = size;
        }
    }

    /**
     * Default rate measurement units.
     */
    private static final Map<String, Integer> RATE_UNIT = new HashMap<String, Integer>();
    static {
        RATE_UNIT.put("Rate  B/s", 1);
        RATE_UNIT.put("Rate KB/s", 1024);
        RATE_UNIT.put("Rate MB/s", 1024 * 1024);
        RATE_UNIT.put("Rate GB/s", 1024 * 1024 * 1024);
    }

    /**
     * Default time measurement units.
     */
    private static final Map<String, Integer> TIME_UNIT = new HashMap<String, Integer>();
    static {
        TIME_UNIT.put("Time ns", 1);
        TIME_UNIT.put("Time us", 1000);
        TIME_UNIT.put("Time ms", 1000 * 1000);
        TIME_UNIT.put("Time s ", 1000 * 1000 * 1000);
    }

    /**
     * Default size measurement units.
     */
    private static final Map<String, Integer> SIZE_UNIT = new HashMap<String, Integer>();
    static {
        SIZE_UNIT.put("Size  B", 1);
        SIZE_UNIT.put("Size KB", 1024);
        SIZE_UNIT.put("Size MB", 1024 * 1024);
        SIZE_UNIT.put("Size GB", 1024 * 1024 * 1024);
    }

    private final Map<Long, Measurement> rateMap = new HashMap<Long, Measurement>();
    private final Map<Long, Measurement> timeMap = new HashMap<Long, Measurement>();
    private final Map<Long, Measurement> sizeMap = new HashMap<Long, Measurement>();

    private final MetricsRegistry metrics = new MetricsRegistry();

    private final Meter rate = metrics.newMeter(getClass(), "rate", "bytes",
            TimeUnit.SECONDS);

    private final Timer time = metrics.newTimer(getClass(), "time",
            TimeUnit.NANOSECONDS, TimeUnit.SECONDS);

    private final SizeGuage size = new SizeGuage();
    {
        metrics.newGauge(getClass(), "", size);
    }

    /**
     * Rate meter.
     */
    public Meter rate() {
        return rate;
    }

    /**
     * Time meter.
     */
    public Timer time() {
        return time;
    }

    /**
     * Size meter.
     */
    public SizeGuage size() {
        return size;
    }

    /**
     * Workaround: zero breaks gwt web app.
     */
    private static double filter(final double value) {
        if (value <= 0.0) {
            return 1.0;
        } else {
            return value;
        }
    }

    /**
     * Perform measurement; convert from metrics into caliper.
     */
    @SuppressWarnings("FloatingPointEquality")
    public void mark() {
        final double rateValue = filter(rate.oneMinuteRate());
        final double timeValue = filter(time.mean());
        final double sizeValue = filter(size.value());
        if (rateValue == 1.0 && timeValue == 1.0 && sizeValue == 1.0) {
            /** ignore complete blank entries */
            return;
        }

        final Measurement markRate = new Measurement(RATE_UNIT, rateValue,
                rateValue);
        rateMap.put(System.nanoTime(), markRate);

        final Measurement markTime = new Measurement(TIME_UNIT, timeValue,
                timeValue);
        timeMap.put(System.nanoTime(), markTime);

        final Measurement markSize = new Measurement(SIZE_UNIT, sizeValue,
                sizeValue);
        sizeMap.put(System.nanoTime(), markSize);
    }

    private final Map<String, String> variables = new HashMap<String, String>();

    /**
     * Caliper scenario variables.
     */
    public Map<String, String> variables() {
        return variables;
    }

    private static MeasurementSet measurementSet(final Map<Long, Measurement> map) {
        final Measurement[] array = map.values().toArray(new Measurement[0]);
        return new MeasurementSet(array);
    }

    /**
     * Attach this measure to parent caliper run.
     */
    public void appendTo(final Run run) {

        final Scenario scenario = new Scenario(variables());

        /** display rate as caliper durations */
        final MeasurementSet timeSet = measurementSet(rateMap);
        final String timeLog = null;

        /** display time as caliper instances */
        final MeasurementSet instSet = measurementSet(timeMap);
        final String instLog = null;

        /** display size as caliper memory */
        final MeasurementSet heapSet = measurementSet(sizeMap);
        final String heapLog = null;

        final ScenarioResult scenarioResult = new ScenarioResult(timeSet,
                timeLog, instSet, instLog, heapSet, heapLog);

        final Map<Scenario, ScenarioResult> measurements = run
                .getMeasurements();

        measurements.put(scenario, scenarioResult);
    }

    /**
     * Terminate metrics resources.
     */
    public void shutdown() {
        rate.stop();
        time.stop();
        metrics.shutdown();
    }
}
