/**
 *  Copyright Terracotta, Inc.
 *
 *  Licensed 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 net.sf.ehcache.pool.sizeof;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Stack;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.sf.ehcache.pool.sizeof.filter.PassThroughFilter;
import net.sf.ehcache.pool.sizeof.filter.SizeOfFilter;

import static net.sf.ehcache.pool.sizeof.JvmInformation.CURRENT_JVM_INFORMATION;

/**
 * SizeOf that uses reflection to measure on heap size of object graphs
 *
 * @author Alex Snaps
 * @author Chris Dennis
 */
public class ReflectionSizeOf extends SizeOf {

    private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionSizeOf.class);

    /**
     * Builds a new SizeOf that will not filter fields and will cache reflected fields
     * @see #ReflectionSizeOf(net.sf.ehcache.pool.sizeof.filter.SizeOfFilter, boolean)
     */
    public ReflectionSizeOf() {
        this(new PassThroughFilter());
    }

    /**
     * Builds a new SizeOf that will filter fields and will cache reflected fields
     * @param fieldFilter The filter to apply
     * @see #ReflectionSizeOf(net.sf.ehcache.pool.sizeof.filter.SizeOfFilter, boolean)
     * @see SizeOfFilter
     */
    public ReflectionSizeOf(SizeOfFilter fieldFilter) {
        this(fieldFilter, true);
    }

    /**
     * Builds a new SizeOf that will filter fields
     * @param fieldFilter The filter to apply
     * @param caching Whether to cache reflected fields
     * @see SizeOfFilter
     */
    public ReflectionSizeOf(SizeOfFilter fieldFilter, boolean caching) {
        super(fieldFilter, caching);

        if (!CURRENT_JVM_INFORMATION.supportsReflectionSizeOf()) {
            LOGGER.warn("ReflectionSizeOf is not always accurate on the JVM (" + CURRENT_JVM_INFORMATION.getJvmDescription() +
                    ").  Please consider enabling AgentSizeOf.");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long sizeOf(Object obj) {
        if (obj == null) {
            return 0;
        }

        Class<?> aClass = obj.getClass();
        if (aClass.isArray()) {
            return guessArraySize(obj);
        } else {
            long size = CURRENT_JVM_INFORMATION.getObjectHeaderSize();

            Stack<Class<?>> classStack = new Stack<Class<?>>();
            for (Class<?> klazz = aClass; klazz != null; klazz = klazz.getSuperclass()) {
                classStack.push(klazz);
            }

            while (!classStack.isEmpty()) {
                Class<?> klazz = classStack.pop();

                //assuming default class layout
                int oops = 0;
                int doubles = 0;
                int words = 0;
                int shorts = 0;
                int bytes = 0;
                for (Field f : klazz.getDeclaredFields()) {
                    if (Modifier.isStatic(f.getModifiers())) {
                        continue;
                    }
                    if (f.getType().isPrimitive()) {
                        switch (PrimitiveType.forType(f.getType())) {
                            case BOOLEAN:
                            case BYTE:
                                bytes++;
                                break;
                            case SHORT:
                            case CHAR:
                                shorts++;
                                break;
                            case INT:
                            case FLOAT:
                                words++;
                                break;
                            case DOUBLE:
                            case LONG:
                                doubles++;
                                break;
                            default:
                                throw new AssertionError();
                        }
                    } else {
                        oops++;
                    }
                }
                if (doubles > 0 && (size % PrimitiveType.LONG.getSize()) != 0) {
                    long length = PrimitiveType.LONG.getSize() - (size % PrimitiveType.LONG.getSize());
                    size += PrimitiveType.LONG.getSize() - (size % PrimitiveType.LONG.getSize());

                    while (length >= PrimitiveType.INT.getSize() && words > 0) {
                        length -= PrimitiveType.INT.getSize();
                        words--;
                    }
                    while (length >= PrimitiveType.SHORT.getSize() && shorts > 0) {
                        length -= PrimitiveType.SHORT.getSize();
                        shorts--;
                    }
                    while (length >= PrimitiveType.BYTE.getSize() && bytes > 0) {
                        length -= PrimitiveType.BYTE.getSize();
                        bytes--;
                    }
                    while (length >= PrimitiveType.getReferenceSize() && oops > 0) {
                        length -= PrimitiveType.getReferenceSize();
                        oops--;
                    }
                }
                size += PrimitiveType.DOUBLE.getSize() * doubles;
                size += PrimitiveType.INT.getSize() * words;
                size += PrimitiveType.SHORT.getSize() * shorts;
                size += PrimitiveType.BYTE.getSize() * bytes;

                if (oops > 0) {
                    if ((size % PrimitiveType.getReferenceSize()) != 0) {
                        size += PrimitiveType.getReferenceSize() - (size % PrimitiveType.getReferenceSize());
                    }
                    size += oops * PrimitiveType.getReferenceSize();
                }

                if ((doubles + words + shorts + bytes + oops) > 0 && (size % CURRENT_JVM_INFORMATION.getPointerSize()) != 0) {
                    size += CURRENT_JVM_INFORMATION.getPointerSize() - (size % CURRENT_JVM_INFORMATION.getPointerSize());
                }
            }
            if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
                size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
            }
            return Math.max(size, CURRENT_JVM_INFORMATION.getMinimumObjectSize());
        }
    }

    private long guessArraySize(Object obj) {
        long size = PrimitiveType.getArraySize();
        int length = Array.getLength(obj);
        if (length != 0) {
            Class<?> arrayElementClazz = obj.getClass().getComponentType();
            if (arrayElementClazz.isPrimitive()) {
                size += length * PrimitiveType.forType(arrayElementClazz).getSize();
            } else {
                size += length * PrimitiveType.getReferenceSize();
            }
        }
        if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
            size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
        }
        return Math.max(size, CURRENT_JVM_INFORMATION.getMinimumObjectSize());
    }
}
