package org.stagemonitor.core.metrics.metrics2;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.Metric;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class Metric2RegistryModule extends Module {

	private final double rateFactor;
	private final double durationFactor;
	private final Metric2Filter filter;

	public Metric2RegistryModule(TimeUnit rateUnit, TimeUnit durationUnit) {
		this(rateUnit, durationUnit, Metric2Filter.ALL);
	}

	public Metric2RegistryModule(TimeUnit rateUnit, TimeUnit durationUnit, Metric2Filter filter) {
		this.rateFactor = rateUnit.toSeconds(1);
		this.durationFactor = 1.0 / durationUnit.toNanos(1);
		this.filter = filter;
	}

	@Override
	public String getModuleName() {
		return "stagemonitor";
	}

	@Override
	public Version version() {
		return new Version(1, 0, 0, "", "org.stagemonitor", "stagemonitor-core");
	}

	@Override
	public void setupModule(SetupContext context) {
		context.addSerializers(new SimpleSerializers(Collections.<JsonSerializer<?>>singletonList(new Metric2RegistrySerializer())));
	}

	private class Metric2RegistrySerializer extends StdSerializer<Metric2Registry> {
		public Metric2RegistrySerializer() {
			super(Metric2Registry.class);
		}

		@Override
		public void serialize(Metric2Registry value, JsonGenerator gen, SerializerProvider provider) throws IOException {
			gen.writeStartArray();
			new MetricSerializer<Gauge>(Gauge.class, new GaugeValueWriter()).serialize(value.getGauges(filter), gen, provider);
			new MetricSerializer<Counter>(Counter.class, new CounterValueWriter()).serialize(value.getCounters(filter), gen, provider);
			new MetricSerializer<Histogram>(Histogram.class, new HistogramValueWriter()).serialize(value.getHistograms(filter), gen, provider);
			new MetricSerializer<Meter>(Meter.class, new MeterValueWriter()).serialize(value.getMeters(filter), gen, provider);
			new MetricSerializer<Timer>(Timer.class, new TimerValueWriter()).serialize(value.getTimers(filter), gen, provider);
			gen.writeEndArray();
		}
	}

	private class MetricSerializer<T extends Metric> extends StdSerializer<Map<MetricName, T>> {
		private final ValueWriter<T> valueWriter;

		public MetricSerializer(Class<T> metricType, ValueWriter<T> valueWriter) {
			super(TypeFactory.defaultInstance().constructMapType(Map.class, MetricName.class, metricType));
			this.valueWriter = valueWriter;
		}

		@Override
		public void serialize(Map<MetricName, T> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
			for (Map.Entry<MetricName, T> entry : value.entrySet()) {
				gen.writeStartObject();
				MetricName metricName = entry.getKey();
				gen.writeStringField("name", metricName.getName());
				gen.writeObjectFieldStart("tags");
				for (Map.Entry<String, String> tagEntry : metricName.getTags().entrySet()) {
					gen.writeObjectField(tagEntry.getKey(), tagEntry.getValue());
				}
				gen.writeEndObject();
				gen.writeObjectFieldStart("values");
				valueWriter.writeValues(entry.getValue(), gen);
				gen.writeEndObject();
				gen.writeEndObject();
			}
		}
	}

	public <T extends Metric> ValueWriter<T> getValueWriter(Class<T> metricClass) {
		if (Gauge.class == metricClass) {
			return (ValueWriter<T>) new GaugeValueWriter();
		} else if (Counter.class == metricClass) {
			return (ValueWriter<T>) new CounterValueWriter();
		} else if (Histogram.class == metricClass) {
			return (ValueWriter<T>) new HistogramValueWriter();
		} else if (Meter.class == metricClass) {
			return (ValueWriter<T>) new MeterValueWriter();
		} else if (Timer.class == metricClass) {
			return (ValueWriter<T>) new TimerValueWriter();
		} else {
			throw new IllegalArgumentException("Unknown metric class: " + metricClass);
		}
	}

	public interface ValueWriter<T extends Metric> {
		void writeValues(T value, JsonGenerator jg) throws IOException;
	}

	private class GaugeValueWriter implements ValueWriter<Gauge> {
		public void writeValues(Gauge gauge, JsonGenerator jg) throws IOException {
			final Object value = gauge.getValue();
			if (value == null) {
				return;
			}
			if (value instanceof Number) {
				writeDoubleUnlessNaN(jg, "value", ((Number) value).doubleValue());
			} else if (value instanceof Boolean) {
				jg.writeBooleanField("value_boolean", (Boolean) value);
			} else {
				jg.writeStringField("value_string", value.toString());
			}
		}
	}

	private class CounterValueWriter implements ValueWriter<Counter> {
		public void writeValues(Counter counter, JsonGenerator jg) throws IOException {
			jg.writeObjectField("count", counter.getCount());
		}
	}

	private class HistogramValueWriter implements ValueWriter<Histogram> {
		public void writeValues(Histogram histogram, JsonGenerator jg) throws IOException {
			final Snapshot snapshot = histogram.getSnapshot();
			jg.writeNumberField("count", histogram.getCount());
			writeHistogramSnapshot(snapshot, jg);
		}
	}

	private class MeterValueWriter implements ValueWriter<Meter> {
		public void writeValues(Meter meter, JsonGenerator jg) throws IOException {
			writeMetered(meter, jg);
		}
	}

	private class TimerValueWriter implements ValueWriter<Timer> {
		public void writeValues(Timer timer, JsonGenerator jg) throws IOException {
			writeMetered(timer, jg);
			writeTimerSnapshot(timer.getSnapshot(), jg);
		}
	}

	private void writeTimerSnapshot(Snapshot snapshot, JsonGenerator jg) throws IOException {
		writeDoubleUnlessNaN(jg, "min", convertDuration(snapshot.getMin()));
		writeDoubleUnlessNaN(jg, "max", convertDuration(snapshot.getMax()));
		writeDoubleUnlessNaN(jg, "mean", convertDuration(snapshot.getMean()));
		writeDoubleUnlessNaN(jg, "p50", convertDuration(snapshot.getMedian()));
		writeDoubleUnlessNaN(jg, "std", convertDuration(snapshot.getStdDev()));
		writeDoubleUnlessNaN(jg, "p25", convertDuration(snapshot.getValue(0.25)));
		writeDoubleUnlessNaN(jg, "p75", convertDuration(snapshot.get75thPercentile()));
		writeDoubleUnlessNaN(jg, "p95", convertDuration(snapshot.get95thPercentile()));
		writeDoubleUnlessNaN(jg, "p98", convertDuration(snapshot.get98thPercentile()));
		writeDoubleUnlessNaN(jg, "p99", convertDuration(snapshot.get99thPercentile()));
		writeDoubleUnlessNaN(jg, "p999", convertDuration(snapshot.get999thPercentile()));
	}
	
	private void writeHistogramSnapshot(Snapshot snapshot, JsonGenerator jg) throws IOException {
		writeDoubleUnlessNaN(jg, "min", snapshot.getMin());
		writeDoubleUnlessNaN(jg, "max", snapshot.getMax());
		writeDoubleUnlessNaN(jg, "mean", snapshot.getMean());
		writeDoubleUnlessNaN(jg, "p50", snapshot.getMedian());
		writeDoubleUnlessNaN(jg, "std", snapshot.getStdDev());
		writeDoubleUnlessNaN(jg, "p25", snapshot.getValue(0.25));
		writeDoubleUnlessNaN(jg, "p75", snapshot.get75thPercentile());
		writeDoubleUnlessNaN(jg, "p95", snapshot.get95thPercentile());
		writeDoubleUnlessNaN(jg, "p98", snapshot.get98thPercentile());
		writeDoubleUnlessNaN(jg, "p99", snapshot.get99thPercentile());
		writeDoubleUnlessNaN(jg, "p999", snapshot.get999thPercentile());
	}

	private void writeMetered(Metered metered, JsonGenerator jg) throws IOException {
		jg.writeNumberField("count", metered.getCount());
		writeDoubleUnlessNaN(jg, "m1_rate", convertRate(metered.getOneMinuteRate()));
		writeDoubleUnlessNaN(jg, "m5_rate", convertRate(metered.getFiveMinuteRate()));
		writeDoubleUnlessNaN(jg, "m15_rate", convertRate(metered.getFifteenMinuteRate()));
		writeDoubleUnlessNaN(jg, "mean_rate", convertRate(metered.getMeanRate()));
	}

	private void writeDoubleUnlessNaN(JsonGenerator jg, String key, double value) throws IOException {
		if (!Double.isNaN(value)) {
			jg.writeNumberField(key, value);
		}
	}


	private double convertDuration(double duration) {
		return duration * durationFactor;
	}

	private double convertRate(double rate) {
		return rate * rateFactor;
	}
}
