/**
 * 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.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;

/**
 * Represents a histogram of values.  This class uses a {@link Segmenter}
 * to determine which bucket to place a given value into. Also stores the last
 * MAX_HISTORY_SIZE entries which have been added to this histogram, in order.
 *
 * Note that Histogram, by itself, is not synchronized.
 * @param <B> Bucket type.  Often String, since buckets are typically
 * used for their toString() representation.
 * @param <T> Type of value
 */
class Histogram<B, T> {
  /**
   * How many recent additions we should track.
   */
  public static final int MAX_HISTORY_SIZE = 20;

  private Segmenter<B, T> segmenter;
  private int[] counts;
  protected int totalCount;
  private LinkedList<T> recentAdditions;

  /**
   * Interface to determine which bucket to place a value in.
   *
   * Segmenters should be immutable, so many histograms can re-use
   * the same segmenter.
   */
  interface Segmenter<B, T> {
    /** Number of buckets to use. */
    int size();
    /**
     * Which bucket to place value in.
     *
     * @return Index of bucket for the value.  At least 0 and less than size().
     * @throws SegmenterException if value does not fit in a bucket.
     */
    int segment(T value);
    /**
     * Returns an iterator of buckets. The order of iteration
     * is consistent with the segment numbers.
     */
    Iterator<B> getBuckets();

    /**
     * Returns a List of bucket boundaries. Useful for printing
     * segmenters.
     */
    List<String> getBoundaryLabels();

    /**
     * Returns the bucket labels as an array;
     */
    List<String> getBucketLabels();
  }

  public static class SegmenterException extends RuntimeException {
    public SegmenterException(String s) {
      super(s);
    }
  }

  public static class TreeMapSegmenter<T extends Comparable<T>>
      implements Segmenter<String, T> {
    private TreeMap<T, Integer> index = new TreeMap<T, Integer>();
    public TreeMapSegmenter(SortedSet<T> leftEndpoints) {
      if (leftEndpoints.isEmpty()) {
        throw new IllegalArgumentException(
            "Endpoints must not be empty: " + leftEndpoints);
      }
      int i = 0;
      for (T t : leftEndpoints) {
        index.put(t, i++);
      }
    }

    public int segment(T value) {
      Map.Entry<T, Integer> e = index.floorEntry(value);
      if (e == null) {
        throw new SegmenterException("Could not find bucket for: " + value);
      }
      return e.getValue();
    }

    @Override
    public int size() {
      return index.size();
    }

    private String rangeAsString(T a, T b) {
      return String.format("[%s,%s)", a, b == null ? "infinity" : b);
    }

    @Override
    public ArrayList<String> getBoundaryLabels() {
      ArrayList<String> outArray = new ArrayList<String>(index.keySet().size());
      for (T obj: index.keySet()) {
        outArray.add(obj.toString());
      }
      return outArray;
    }

    @Override
    public ArrayList<String> getBucketLabels() {
      ArrayList<String> outArray = new ArrayList<String>(index.keySet().size());
      Iterator<String> bucketsIt = this.getBuckets();
      while (bucketsIt.hasNext()) {
        outArray.add(bucketsIt.next());
      }
      return outArray;
    }

    @Override
    public Iterator<String> getBuckets() {
      return new Iterator<String>() {
        Iterator<T> it = index.keySet().iterator();
        T cur = it.next(); // there's always at least one element
        int pos = 0;

        @Override
        public boolean hasNext() {
          return (pos < index.keySet().size());
        }

        @Override
        public String next() {
          pos = pos + 1;
          T left = cur;
          cur = it.hasNext() ? it.next() : null;
          return rangeAsString(left, cur);
        }

        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  }

  /**
   * Creates a histogram using the specified segmenter.
   */
  public Histogram(Segmenter<B, T> segmenter) {
    this.segmenter = segmenter;
    this.counts = new int[segmenter.size()];
    this.recentAdditions = new LinkedList<T>();
  }

  /** Tallies a value in the histogram. */
  public void add(T value) {
    int i = segmenter.segment(value);
    counts[i]++;
    totalCount++;
    if (this.recentAdditions.size() > Histogram.MAX_HISTORY_SIZE) {
      this.recentAdditions.pollLast();
    }
    this.recentAdditions.push(value);
  }

  /**
   * Returns the underlying bucket values.
   */
  public int[] getHistogram() {
    return counts;
  }

  /**
   * Returns the underlying segmenter used for this histogram.
   */
  public Segmenter<B, T> getSegmenter() {
    return this.segmenter;
  }

  /**
   * Returns values recently added to this histogram. These are in reverse
   * order (most recent first).
   */
  public List<T> getRecentAdditions() {
    return this.recentAdditions;
  }

  /** Returns the total count of entries. */
  public int getCount() {
    return totalCount;
  }


  public String toString() {
    StringBuilder sb = new StringBuilder();
    boolean first = true;
    for (Entry<B> e : entries()) {
      if (!first) {
        sb.append(";");
      } else {
        first = false;
      }
      sb.append(e.bucket).append("=").append(e.count);
    }
    return sb.toString();
  }

  static class Entry<B> {
    public Entry(B bucket, int count) {
      this.bucket = bucket;
      this.count = count;
    }
    B bucket;
    int count;
  }

  private class EntryIterator implements Iterable<Entry<B>>, Iterator<Entry<B>> {
    int i = 0;
    Iterator<B> bucketNameIterator = segmenter.getBuckets();

    @Override
    public Iterator<Entry<B>> iterator() {
      return this;
    }

    @Override
    public boolean hasNext() {
      return i < segmenter.size();
    }

    @Override
    public Entry<B> next() {
      return new Entry<B>(bucketNameIterator.next(), counts[i++]);
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException();
    }

  }

  public Iterable<Entry<B>> entries() {
    return new EntryIterator();
  }
}
