/*
 * 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.geode.internal.size;

import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.size.ObjectTraverser.Visitor;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;


public class ObjectGraphSizer {
  private static final String SIZE_OF_CLASS_NAME =
      System.getProperty(DistributionConfig.GEMFIRE_PREFIX + "ObjectSizer.SIZE_OF_CLASS",
          ReflectionSingleObjectSizer.class.getName());
  static final SingleObjectSizer SIZE_OF_UTIL;
  private static ObjectFilter NULL_FILTER = new ObjectFilter() {
    @Override
    public boolean accept(Object parent, Object object) {
      return true;
    }

  };

  static {
    Class sizeOfClass;
    try {
      sizeOfClass = ClassPathLoader.getLatest().forName(SIZE_OF_CLASS_NAME);
      SIZE_OF_UTIL = new CachingSingleObjectSizer((SingleObjectSizer) sizeOfClass.newInstance());
    } catch (Exception e) {
      throw new ExceptionInInitializerError(e);
    }
  }

  /**
   * Find the size of an object and all objects reachable from it using breadth first search. This
   * is equivalent to calling
   * 
   * <pre>
   * size(root, false);
   * </pre>
   * 
   */
  // TODO
  // - native byte buffers
  // - native memory (how??)
  public static long size(Object root) throws IllegalArgumentException, IllegalAccessException {
    return size(root, false);
  }

  /**
   * Find the size of an object and all objects reachable from it using breadth first search. This
   * is equivalent to calling set(root, filter, includeStatics) where the filter will accept all
   * objects.
   * 
   */
  public static long size(Object root, boolean includeStatics)
      throws IllegalArgumentException, IllegalAccessException {
    return size(root, NULL_FILTER, includeStatics);
  }

  /**
   * Find the size of an object and all objects reachable from it using breadth first search. This
   * method will include objects reachable from static fields. Using this method requires some heap
   * space - probably between 8 - 30 bytes per reachable object.
   * 
   * Objects reachable only through weak or soft references will not be considered part of the total
   * size.
   * 
   * @param root the object to size
   * @param filter that can exclude objects from being counted in the results. If an object is not
   *        accepted, it's size will not be included and it's children will not be visited unless
   *        they are reachable by some other path.
   * @param includeStatics if set to true, static members of a class will be traversed the first
   *        time that a class is encountered.
   * 
   */
  public static long size(Object root, ObjectFilter filter, boolean includeStatics)
      throws IllegalArgumentException, IllegalAccessException {
    SizeVisitor visitor = new SizeVisitor(filter);
    ObjectTraverser.breadthFirstSearch(root, visitor, includeStatics);

    return visitor.getTotalSize();
  }

  public static String histogram(Object root, boolean includeStatics)
      throws IllegalArgumentException, IllegalAccessException {
    return histogram(root, NULL_FILTER, includeStatics);
  }

  public static String histogram(Object root, ObjectFilter filter, boolean includeStatics)
      throws IllegalArgumentException, IllegalAccessException {
    HistogramVistor visitor = new HistogramVistor(filter);
    ObjectTraverser.breadthFirstSearch(root, visitor, includeStatics);

    return visitor.dump();

  }

  private static class HistogramVistor implements ObjectTraverser.Visitor {
    private final Map<Class, Integer> countHisto = new HashMap<Class, Integer>();
    private final Map<Class, Long> sizeHisto = new HashMap<Class, Long>();
    private final ObjectFilter filter;

    public HistogramVistor(ObjectFilter filter) {
      this.filter = filter;
    }

    public boolean visit(Object parent, Object object) {
      if (!filter.accept(parent, object)) {
        return false;
      }
      Integer count = countHisto.get(object.getClass());
      if (count == null) {
        count = Integer.valueOf(1);
      } else {
        count = Integer.valueOf(count.intValue() + 1);
      }

      countHisto.put(object.getClass(), count);

      long objectSize;
      try {
        objectSize = SIZE_OF_UTIL.sizeof(object);
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      }
      Long size = sizeHisto.get(object.getClass());
      if (size == null) {
        size = Long.valueOf(objectSize);
      } else {
        size = Long.valueOf(size.longValue() + objectSize);
      }

      sizeHisto.put(object.getClass(), size);

      return true;
    }

    public String dump() {
      StringBuilder result = new StringBuilder();
      result.append("clazz\tsize\tcount\n");
      Set<HistogramEntry> orderedSize = getOrderedSet();
      for (HistogramEntry entry : orderedSize) {
        Class clazz = entry.clazz;
        Integer count = entry.count;
        Long size = entry.size;
        result.append(clazz + "\t" + size + "\t" + count + "\n");
      }
      return result.toString();
    }

    public Set<HistogramEntry> getOrderedSet() {
      TreeSet<HistogramEntry> result = new TreeSet<HistogramEntry>();
      for (Map.Entry<Class, Long> entry : sizeHisto.entrySet()) {
        Class clazz = entry.getKey();
        Long size = entry.getValue();
        Integer count = countHisto.get(clazz);
        result.add(new HistogramEntry(clazz, count, size));
      }
      return result;
    }

    private static class HistogramEntry implements Comparable<HistogramEntry> {
      private final Class clazz;
      private final Integer count;
      private final Long size;

      public HistogramEntry(Class clazz, Integer count, Long size) {
        this.size = size;
        this.clazz = clazz;
        this.count = count;
      }



      public int compareTo(HistogramEntry o) {
        int diff = size.compareTo(o.size);
        if (diff == 0) {
          diff = clazz.getName().compareTo(o.clazz.getName());
        }
        return diff;
      }

      @Override
      public String toString() {
        return size.toString();
      }
    }
  }

  private static class SizeVisitor implements Visitor {
    private long totalSize;
    private final ObjectFilter filter;

    public SizeVisitor(ObjectFilter filter) {
      this.filter = filter;
    }

    public boolean visit(Object parent, Object object) {
      if (!filter.accept(parent, object)) {
        return false;
      }

      totalSize += SIZE_OF_UTIL.sizeof(object);
      // We do want to include the size of the reference itself, but
      // we don't visit the children because they will be GC'd if there is no
      // other reference
      return !(object instanceof WeakReference) && !(object instanceof SoftReference);
    }

    public long getTotalSize() {
      return totalSize;
    }
  }



  public static interface ObjectFilter {
    boolean accept(Object parent, Object object);
  }

  private ObjectGraphSizer() {}

}
