package org.apache.blur.metrics;

/**
 * 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.
 */
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Metered;
import com.yammer.metrics.core.Metric;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricProcessor;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.reporting.AbstractPollingReporter;
import com.yammer.metrics.stats.Snapshot;

public class JSONReporter extends AbstractPollingReporter implements MetricProcessor<JSONReporter.Context> {

  private static final ResetableCharArrayWriter EMPTY = new ResetableCharArrayWriter() {
    {
      try {
        write("[]");
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
  };
  private static Log LOG = LogFactory.getLog(JSONReporter.class);

  private Context context;
  private int _256K = 262144;
  private ResetableCharArrayWriter writerInUse = new ResetableCharArrayWriter(_256K);
  private ResetableCharArrayWriter writerWriting = new ResetableCharArrayWriter(_256K);
  private static AtomicReference<ResetableCharArrayWriter> reading = new AtomicReference<ResetableCharArrayWriter>(
      EMPTY);

  public static void enable(String name, long period, TimeUnit unit, int numberOfElements) throws IOException {
    enable(Metrics.defaultRegistry(), name, period, unit, numberOfElements);
  }

  public static void enable(MetricsRegistry metricsRegistry, String name, long period, TimeUnit unit,
      int numberOfElements) throws IOException {
    JSONReporter reporter = new JSONReporter(metricsRegistry, name, numberOfElements);
    reporter.start(period, unit);
  }

  public static void writeJSONData(Writer writer) throws IOException {
    synchronized (reading) {
      ResetableCharArrayWriter reader = reading.get();
      writer.write(reader.getBuffer(), 0, reader.size());
    }
  }

  protected JSONReporter(MetricsRegistry registry, String name, int numberOfElements) {
    super(registry, name);
    this.context = new Context(numberOfElements);
  }

  static class Context {

    private final Map<MetricName, MetricInfo> metricInfoMap = new HashMap<MetricName, MetricInfo>();
    private final Map<MetricName, String> typeTable;
    private final int numberOfElements;
    private long time;

    Context(int numberOfElements) {
      this.typeTable = new HashMap<MetricName, String>();
      this.numberOfElements = numberOfElements;
    }

    long getTime() {
      return time;
    }

    void setTime(long time) {
      this.time = time;
    }

    public MetricInfo getMetricInfo(MetricName name) {
      MetricInfo info = metricInfoMap.get(name);
      if (info == null) {
        info = new MetricInfo(getName(name), typeTable.get(name), numberOfElements);
        metricInfoMap.put(name, info);
      }
      return info;
    }

    private String getName(MetricName metricName) {
      // String group = metricName.getGroup();
      // String name = metricName.getName();
      // String scope = metricName.getScope();
      // String type = metricName.getType();
      // JSONObject jsonObject = new JSONObject();
      // jsonObject.put("name", name);
      // jsonObject.put("group", group);
      // jsonObject.put("scope", scope);
      // jsonObject.put("type", type);
      return metricName.toString();
    }
  }

  @Override
  public void run() {
    try {
      context.setTime(System.currentTimeMillis());
      for (Entry<String, SortedMap<MetricName, Metric>> entry : getMetricsRegistry().groupedMetrics().entrySet()) {
        for (Entry<MetricName, Metric> subEntry : entry.getValue().entrySet()) {
          MetricName name = subEntry.getKey();
          Metric metric = subEntry.getValue();
          if (metric instanceof Counter) {
            context.typeTable.put(name, "counter");
          } else if (metric instanceof Gauge) {
            context.typeTable.put(name, "gauge");
          } else if (metric instanceof Histogram) {
            context.typeTable.put(name, "histogram");
          } else if (metric instanceof Meter) {
            context.typeTable.put(name, "meter");
          } else if (metric instanceof Timer) {
            context.typeTable.put(name, "timer");
          }
          metric.processWith(this, name, context);
        }
      }
      ResetableCharArrayWriter writer = getWriter();
      writer.reset();
      Set<Entry<MetricName, MetricInfo>> entrySet = context.metricInfoMap.entrySet();
      writer.append('[');
      boolean flag = false;
      for (Entry<MetricName, MetricInfo> entry : entrySet) {
        if (flag) {
          writer.append(',');
        }
        entry.getValue().write(writer);
        flag = true;
      }
      writer.append(']');
      swapWriter();
    } catch (Throwable t) {
      LOG.error("Unknown error during the processing of metrics.", t);
    }
  }

  private void swapWriter() {
    synchronized (reading) {
      ResetableCharArrayWriter tmp1 = writerWriting;
      writerWriting = writerInUse;
      writerInUse = tmp1;
      reading.set(writerInUse);
    }
  }

  private ResetableCharArrayWriter getWriter() {
    return writerWriting;
  }

  @Override
  public void processMeter(MetricName name, Metered meter, Context context) throws Exception {
    MetricInfo info = context.getMetricInfo(name);
    long time = context.getTime();
    addMeterInfo(time, meter, info);
  }

  @Override
  public void processCounter(MetricName name, Counter counter, Context context) throws Exception {
    MetricInfo info = context.getMetricInfo(name);
    long time = context.getTime();
    info.addNumber("timestamp", time);
    info.addNumber("value", counter.count());
  }

  @Override
  public void processHistogram(MetricName name, Histogram histogram, Context context) throws Exception {
    MetricInfo info = context.getMetricInfo(name);
    long time = context.getTime();
    info.addNumber("timestamp", time);
    info.addNumber("min", histogram.min());
    info.addNumber("max", histogram.max());
    info.addNumber("mean", histogram.mean());
    info.addNumber("stdDev", histogram.stdDev());

    Snapshot snapshot = histogram.getSnapshot();
    info.addNumber("median", snapshot.getMedian());
    info.addNumber("75%", snapshot.get75thPercentile());
    info.addNumber("95%", snapshot.get95thPercentile());
    info.addNumber("98%", snapshot.get98thPercentile());
    info.addNumber("99%", snapshot.get99thPercentile());
    info.addNumber("99.9%", snapshot.get999thPercentile());
  }

  @Override
  public void processTimer(MetricName name, Timer timer, Context context) throws Exception {
    MetricInfo info = context.getMetricInfo(name);
    long time = context.getTime();

    addMeterInfo(time, timer, info);
    info.setMetaData("unit", timer.durationUnit().toString());
    info.addNumber("min", timer.min());
    info.addNumber("max", timer.max());
    info.addNumber("mean", timer.mean());
    info.addNumber("stdDev", timer.stdDev());

    Snapshot snapshot = timer.getSnapshot();
    info.addNumber("median", snapshot.getMedian());
    info.addNumber("75%", snapshot.get75thPercentile());
    info.addNumber("95%", snapshot.get95thPercentile());
    info.addNumber("98%", snapshot.get98thPercentile());
    info.addNumber("99%", snapshot.get99thPercentile());
    info.addNumber("99.9%", snapshot.get999thPercentile());
  }

  @Override
  public void processGauge(MetricName name, Gauge<?> gauge, Context context) throws Exception {
    MetricInfo info = context.getMetricInfo(name);
    long time = context.getTime();
    info.addNumber("timestamp", time);
    info.addNumber("value", getDouble(gauge.value()));
  }

  private double getDouble(Object value) {
    if (value instanceof Integer) {
      Integer v = (Integer) value;
      return (int) v;
    } else if (value instanceof Long) {
      Long v = (Long) value;
      return (long) v;
    } else if (value instanceof Double) {
      Double v = (Double) value;
      return v;
    } else if (value instanceof Float) {
      Float v = (Float) value;
      return (float) v;
    }
    return 0;
  }

  private void addMeterInfo(Long time, Metered meter, MetricInfo info) {
    info.addNumber("timestamp", time);
    info.setMetaData("rateUnit", meter.rateUnit().toString());
    info.setMetaData("eventType", meter.eventType());
    info.addNumber("count", meter.count());
    info.addNumber("meanRate", meter.meanRate());
    info.addNumber("oneMinuteRate", meter.oneMinuteRate());
    info.addNumber("fiveMinuteRate", meter.fiveMinuteRate());
    info.addNumber("fifteenMinuteRate", meter.fifteenMinuteRate());
  }

}