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

import java.nio.ByteBuffer;
import java.nio.LongBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Random;

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;

/**
 * Utility methods for common tasks in Avro tracing. Mostly consists of
 * static methods which we can't put in auto-generated classes.
 */
class Util {
  final private static Random RANDOM = new Random();
  final private static int NANOS_PER_MILI = 1000000;
  
  /**
   * Get all SpanEvents contained in Span s.
   */
  public static EnumSet<SpanEvent> getAllEvents(Span s) {
    EnumSet<SpanEvent> foundEvents = EnumSet.noneOf(SpanEvent.class);
    for (TimestampedEvent event: s.events) {
      if (event.event instanceof SpanEvent) {
        foundEvents.add((SpanEvent) event.event);
      }
    }
    return foundEvents;
  }
  
  /**
   * Get the size of an RPC payload.
   */
  public static int getPayloadSize(List<ByteBuffer> payload) {
    if (payload == null) {
      return 0;
    }
    int size = 0;
    for (ByteBuffer bb: payload) {
      size = size + bb.limit();
    }
    return size;
  }
  
  /**
   * Create a span without any events. If traceID or spanID is null, randomly
   * generate them. If parentSpanID is null, assume this is a root span.
   */
  public static Span createEventlessSpan(ID traceID, ID spanID, ID parentSpanID) {
    Span span = new Span();
    span.complete = false;
    
    if (traceID == null) {
      byte[] traceIDBytes = new byte[8];
      RANDOM.nextBytes(traceIDBytes);
      span.traceID = new ID();
      span.traceID.bytes(traceIDBytes);
    } else {
      span.traceID = new ID();
      span.traceID.bytes(traceID.bytes().clone());
    }
    
    if (spanID == null) {
      byte[] spanIDBytes = new byte[8];
      RANDOM.nextBytes(spanIDBytes);
      span.spanID = new ID();
      span.spanID.bytes(spanIDBytes);
    } else {
      span.spanID = new ID();
      span.spanID.bytes(spanID.bytes().clone());
    }

    if (parentSpanID != null) {
      span.parentSpanID = new ID();
      span.parentSpanID.bytes(parentSpanID.bytes().clone());
    }
    
    span.events = new GenericData.Array<TimestampedEvent>(
        10, Schema.createArray(TimestampedEvent.SCHEMA$));
    return span;
  }
  
  /**
   * Add a TimestampedEvent to a Span using the current time. 
   */
  public static void addEvent(Span span, SpanEvent eventType) {
    TimestampedEvent ev = new TimestampedEvent();
    ev.event = eventType;
    ev.timeStamp = System.currentTimeMillis() * NANOS_PER_MILI;
    span.events.add(ev);
  }
  
  /**
   * Get the long value from a given ID object.
   */
  public static long longValue(ID in) {
    if (in == null) { 
      throw new IllegalArgumentException("ID cannot be null");
    }
    if (in.bytes() == null) {
      throw new IllegalArgumentException("ID cannot be empty");
    }
    if (in.bytes().length != 8) {
      throw new IllegalArgumentException("ID must be 8 bytes");
    }
    ByteBuffer buff = ByteBuffer.wrap(in.bytes());
    return buff.getLong();
  }
  
  /**
   * Get an ID associated with a given long value. 
   */
  public static ID idValue(long in) {
    byte[] bArray = new byte[8];
    ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
    LongBuffer lBuffer = bBuffer.asLongBuffer();
    lBuffer.put(0, in);
    ID out = new ID();
    out.bytes(bArray);
    return out;
  }
  
  /**
   * Verify the equality of ID objects. Both being null references is
   * considered equal.
   */
  public static boolean idsEqual(ID a, ID b) {
    if (a == null && b == null) { return true; }
    if (a == null || b == null) { return false; }
    
    return Arrays.equals(a.bytes(), b.bytes());
  }
  
  /**
   * Convert a timeStamp (in nanoseconds) to a pretty string.
   */
  public static String printableTime(long stamp) {
    String out = "";
    double milliseconds = (double) stamp / (double) NANOS_PER_MILI;
    return String.format("%.2fms", milliseconds);
  }
  
  /**
   * Convert a bytes count to a pretty string.
   */
  public static String printableBytes(long bytes) {
    if (bytes < 1024) {
      return Long.toString(bytes) + "b";
    } else if (bytes < (1024 * 1024)) {
      double kb = (double) bytes / 1024.0;
      return String.format("%.2fkb", kb);
    } else {
      double mb = (double) bytes / (1024.0 * 1024.0);
      return String.format("%.2fmb", mb);
    }
  }

  /**
   * Tests if a span occurred between start and end.
   */
  public static boolean spanInRange(Span s, long start, long end) {
    long startTime = 0;
    long endTime = 0;
    
    for (TimestampedEvent e: s.events) {
      if (e.event instanceof SpanEvent) {
        SpanEvent ev = (SpanEvent) e.event;
        switch (ev) {
          case CLIENT_SEND: startTime = e.timeStamp;
          case SERVER_RECV: startTime = e.timeStamp;
          case CLIENT_RECV: endTime = e.timeStamp;
          case SERVER_SEND: endTime = e.timeStamp;
        }      
      }
    }
    if (startTime > start && endTime < end) { return true; }
    return false;
  }
  
  /**
   * Return a copy of input that contains no more than maxEntries items.
   * If the input list is more than maxEntries items long, the original list
   * is uniformly sampled.
   * 
   * This uses a very simple sampling technique and may result in lists
   * smaller than maxEntries.
   */
  public static <T> List<T> sampledList(List<T> input, int maxEntries) {
    int timesTooLarge = (int) Math.ceil((double) input.size() / maxEntries);
    if (timesTooLarge <= 1) { return new ArrayList<T>(input); }
    
    ArrayList<T> out = new ArrayList<T>();
    for (int i = 0; i < input.size(); i++) {
      if (i % timesTooLarge == 0) {
        out.add(input.get(i));
      }
    } 
    return out;
  }
}
