/*
 * 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.ode.agents.memory;

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Calendar;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;


/**
 * Class
 *
 * @author $author$
 * @version $Revision$
  */
public class SizingAgent {
    private static Instrumentation instrumentation;

    /**
     * $method$
     *
     * @param options options
     * @param instrumentation instrumentation
     */
    public static void premain(String options, Instrumentation instrumentation) {
        SizingAgent.instrumentation = instrumentation;
        Class[] loaded = instrumentation.getAllLoadedClasses();
    }

    /**
     * $method$
     *
     * @param args args
     */
    public static void main(String[] args) {
        System.out.println("Size of Object: " + sizeOf(new Object()));
        System.out.println("Size of direct subclass: " + sizeOf(new SizingAgent()));
        System.out.println("Size of Calendar: " +
            sizeOf(Calendar.getInstance()));
    }

    /**
     * Returns object size.
     */
    public static long sizeOf(Object obj) {
        if (instrumentation == null) {
            return 0;
        }

        if (isSharedFlyweight(obj)) {
            return 0;
        }

        return instrumentation.getObjectSize(obj);
    }

    /**
     * Returns deep size of object, recursively iterating over its
     * fields and superclasses.
     */
    public static long deepSizeOf(Object obj) {
        if (instrumentation == null) {
            return 0;
        }

        Map visited = new IdentityHashMap();
        Stack stack = new Stack();
        stack.push(obj);

        long result = 0;

        do {
            result += internalSizeOf(stack.pop(), stack, visited);
        } while (!stack.isEmpty());

        return result;
    }

    /**
     * Returns true if this is a well-known shared flyweight. For
     * example, interned Strings, Booleans and Number objects
     */
    private static boolean isSharedFlyweight(Object obj) {
        // optimization - all of our flyweights are Comparable
        if (obj instanceof Comparable) {
            if (obj instanceof Enum) {
                return true;
            } else if (obj instanceof String) {
                return (obj == ((String) obj).intern());
            } else if (obj instanceof Boolean) {
                return ((obj == Boolean.TRUE) || (obj == Boolean.FALSE));
            } else if (obj instanceof Integer) {
                return (obj == Integer.valueOf((Integer) obj));
            } else if (obj instanceof Short) {
                return (obj == Short.valueOf((Short) obj));
            } else if (obj instanceof Byte) {
                return (obj == Byte.valueOf((Byte) obj));
            } else if (obj instanceof Long) {
                return (obj == Long.valueOf((Long) obj));
            } else if (obj instanceof Character) {
                return (obj == Character.valueOf((Character) obj));
            }
        }

        return false;
    }

    /**
     * $method$
     *
     * @param obj obj
     * @param visited visited
     *
     * @return type
     */
    private static boolean skipObject(Object obj, Map visited) {
        return (obj == null) || visited.containsKey(obj) ||
        isSharedFlyweight(obj);
    }

    /**
     * $method$
     *
     * @param obj obj
     * @param stack stack
     * @param visited visited
     *
     * @return type
     */
    private static long internalSizeOf(Object obj, Stack stack, Map visited) {
        if (skipObject(obj, visited)) {
            return 0;
        }

        Class clazz = obj.getClass();

        if (clazz.isArray()) {
            addArrayElementsToStack(clazz, obj, stack);
        } else {
            // add all non-primitive fields to the stack
            while (clazz != null) {
                Field[] fields = clazz.getDeclaredFields();

                for (Field field : fields) {
                    if (!Modifier.isStatic(field.getModifiers()) &&
                            !field.getType().isPrimitive()) {
                        field.setAccessible(true);

                        try {
                            stack.add(field.get(obj));
                        } catch (IllegalAccessException ex) {
                            throw new RuntimeException(ex);
                        }
                    }
                }

                clazz = clazz.getSuperclass();
            }
        }

        visited.put(obj, null);

        return sizeOf(obj);
    }

    /**
     * $method$
     *
     * @param clazz clazz
     * @param obj obj
     * @param stack stack
     */
    private static void addArrayElementsToStack(Class clazz, Object obj,
        Stack stack) {
        if (!clazz.getComponentType().isPrimitive()) {
            int length = Array.getLength(obj);

            for (int i = 0; i < length; i++) {
                stack.add(Array.get(obj, i));
            }
        }
    }
}
