/**
 * 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.avro.ipc.stats;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.avro.Protocol.Message;
import org.apache.avro.ipc.RPCContext;
import org.apache.avro.ipc.RPCPlugin;
import org.apache.avro.ipc.stats.Histogram.Segmenter;
import org.apache.avro.ipc.stats.Stopwatch.Ticks;

/**
 * Collects count and latency statistics about RPC calls.  Keeps
 * data for every method. Can be added to a Requestor (client)
 * or Responder (server).
 *
 * This uses milliseconds as the standard unit of measure
 * throughout the class, stored in floats.
 */
public class StatsPlugin extends RPCPlugin {
  /** Static declaration of histogram buckets. */
  static final Segmenter<String, Float> LATENCY_SEGMENTER =
    new Histogram.TreeMapSegmenter<Float>(new TreeSet<Float>(Arrays.asList(
            0f,
           25f,
           50f,
           75f,
          100f,
          200f,
          300f,
          500f,
          750f,
         1000f, // 1 second
         2000f,
         5000f,
        10000f,
        60000f, // 1 minute
       600000f)));

  static final Segmenter<String, Integer> PAYLOAD_SEGMENTER =
    new Histogram.TreeMapSegmenter<Integer>(new TreeSet<Integer>(Arrays.asList(
            0,
           25,
           50,
           75,
          100,
          200,
          300,
          500,
          750,
         1000, // 1 k
         2000,
         5000,
        10000,
        50000,
       100000)));

  /** Per-method histograms.
   * Must be accessed while holding a lock. */
  Map<Message, FloatHistogram<?>> methodTimings =
    new HashMap<Message, FloatHistogram<?>>();

  Map<Message, IntegerHistogram<?>> sendPayloads =
    new HashMap<Message, IntegerHistogram<?>>();

  Map<Message, IntegerHistogram<?>> receivePayloads =
    new HashMap<Message, IntegerHistogram<?>>();

  /** RPCs in flight. */
  ConcurrentMap<RPCContext, Stopwatch> activeRpcs =
    new ConcurrentHashMap<RPCContext, Stopwatch>();
  private Ticks ticks;

  /** How long I've been alive */
  public Date startupTime = new Date();

  private Segmenter<?, Float> floatSegmenter;
  private Segmenter<?, Integer> integerSegmenter;

  /** Construct a plugin with custom Ticks and Segmenter implementations. */
  StatsPlugin(Ticks ticks, Segmenter<?, Float> floatSegmenter,
      Segmenter<?, Integer> integerSegmenter) {
    this.floatSegmenter = floatSegmenter;
    this.integerSegmenter = integerSegmenter;
    this.ticks = ticks;
  }

  /** Construct a plugin with default (system) ticks, and default
   * histogram segmentation. */
  public StatsPlugin() {
    this(Stopwatch.SYSTEM_TICKS, LATENCY_SEGMENTER, PAYLOAD_SEGMENTER);
  }

  /**
   * Helper to get the size of an RPC payload.
   */
  private int getPayloadSize(List<ByteBuffer> payload) {
    if (payload == null) {
      return 0;
    }

    int size = 0;
    for (ByteBuffer bb: payload) {
      size = size + bb.limit();
    }

    return size;
  }

  @Override
  public void serverReceiveRequest(RPCContext context) {
    Stopwatch t = new Stopwatch(ticks);
    t.start();
    this.activeRpcs.put(context, t);

    synchronized(receivePayloads) {
      IntegerHistogram<?> h = receivePayloads.get(context.getMessage());
      if (h == null) {
        h = createNewIntegerHistogram();
        receivePayloads.put(context.getMessage(), h);
      }
      h.add(getPayloadSize(context.getRequestPayload()));
    }
  }

  @Override
  public void serverSendResponse(RPCContext context) {
    Stopwatch t = this.activeRpcs.remove(context);
    t.stop();
    publish(context, t);

    synchronized(sendPayloads) {
      IntegerHistogram<?> h = sendPayloads.get(context.getMessage());
      if (h == null) {
        h = createNewIntegerHistogram();
        sendPayloads.put(context.getMessage(), h);
      }
      h.add(getPayloadSize(context.getResponsePayload()));
    }
  }

  @Override
  public void clientSendRequest(RPCContext context) {
    Stopwatch t = new Stopwatch(ticks);
    t.start();
    this.activeRpcs.put(context, t);

    synchronized(sendPayloads) {
      IntegerHistogram<?> h = sendPayloads.get(context.getMessage());
      if (h == null) {
        h = createNewIntegerHistogram();
       sendPayloads.put(context.getMessage(), h);
      }
      h.add(getPayloadSize(context.getRequestPayload()));
    }
  }

  @Override
  public void clientReceiveResponse(RPCContext context) {
    Stopwatch t = this.activeRpcs.remove(context);
    t.stop();
    publish(context, t);

    synchronized(receivePayloads) {
      IntegerHistogram<?> h = receivePayloads.get(context.getMessage());
      if (h == null) {
        h = createNewIntegerHistogram();
        receivePayloads.put(context.getMessage(), h);
      }
      h.add(getPayloadSize(context.getRequestPayload()));
    }
  }

  /** Adds timing to the histograms. */
  private void publish(RPCContext context, Stopwatch t) {
    Message message = context.getMessage();
    if (message == null) throw new IllegalArgumentException();
    synchronized(methodTimings) {
      FloatHistogram<?> h = methodTimings.get(context.getMessage());
      if (h == null) {
        h = createNewFloatHistogram();
        methodTimings.put(context.getMessage(), h);
      }
      h.add(nanosToMillis(t.elapsedNanos()));
    }
  }

  @SuppressWarnings("unchecked")
  private FloatHistogram<?> createNewFloatHistogram() {
    return new FloatHistogram(floatSegmenter);
  }

  @SuppressWarnings("unchecked")
  private IntegerHistogram<?> createNewIntegerHistogram() {
    return new IntegerHistogram(integerSegmenter);
  }

  /** Converts nanoseconds to milliseconds. */
  static float nanosToMillis(long elapsedNanos) {
    return elapsedNanos / 1000000.0f;
  }
}
