package com.strobel.assembler.metadata;

import org.apache.commons.collections4.map.LRUMap;

import com.strobel.core.VerifyArgument;

/**
 * Keeps a set of types which failed to load and also uses an LRUCache to reduce the memory footprint for cached lookups.
 *
 * @author <a href="mailto:ozizka@redhat.com">Ondrej Zizka</a>
 */
public final class NoRetryMetadataSystem extends WindupMetadataSystem {
    private final static TypeDefinition[] PRIMITIVE_TYPES_BY_NAME = new TypeDefinition['Z' - 'B' + 1];
    private final static TypeDefinition[] PRIMITIVE_TYPES_BY_DESCRIPTOR = new TypeDefinition[16];
    private final LRUMap<String, Boolean> failedTypes = new LRUMap<>(1000);
    private final LRUMap<String, TypeDefinition> resolvedTypes = new LRUMap<>(1000);
    private final ITypeLoader typeLoader;

    public NoRetryMetadataSystem(final ITypeLoader typeLoader) {
        super(typeLoader);
        this.typeLoader = typeLoader;
    }

    public void addTypeDefinition(final TypeDefinition type) {
        VerifyArgument.notNull(type, "type");
        resolvedTypes.put(type.getInternalName(), type);
    }

    @Override
    protected TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) {
        if (failedTypes.containsKey(descriptor))
            return null;

        TypeDefinition result = resolveTypeInternal(descriptor, mightBePrimitive);

        if (result == null)
            failedTypes.put(descriptor, true);

        return result;
    }

    protected TypeDefinition resolveTypeInternal(final String descriptor, final boolean mightBePrimitive) {
        VerifyArgument.notNull(descriptor, "descriptor");

        if (mightBePrimitive) {
            if (descriptor.length() == 1) {
                final int primitiveHash = descriptor.charAt(0) - 'B';

                if (primitiveHash >= 0 && primitiveHash < PRIMITIVE_TYPES_BY_DESCRIPTOR.length) {
                    final TypeDefinition primitiveType = PRIMITIVE_TYPES_BY_DESCRIPTOR[primitiveHash];

                    if (primitiveType != null) {
                        return primitiveType;
                    }
                }
            } else {
                final int primitiveHash = hashPrimitiveName(descriptor);

                if (primitiveHash >= 0 && primitiveHash < PRIMITIVE_TYPES_BY_NAME.length) {
                    final TypeDefinition primitiveType = PRIMITIVE_TYPES_BY_NAME[primitiveHash];

                    if (primitiveType != null && descriptor.equals(primitiveType.getName())) {
                        return primitiveType;
                    }
                }
            }
        }

        TypeDefinition cachedDefinition = resolvedTypes.get(descriptor);

        if (cachedDefinition != null) {
            return cachedDefinition;
        }

        final Buffer buffer = new Buffer(0);

        if (!this.typeLoader.tryLoadType(descriptor, buffer)) {
            return null;
        }

        final TypeDefinition typeDefinition = ClassFileReader.readClass(ClassFileReader.OPTIONS_DEFAULT, this, buffer);

        cachedDefinition = resolvedTypes.put(descriptor, typeDefinition);
        typeDefinition.setTypeLoader(this.typeLoader);

        if (cachedDefinition != null) {
            return cachedDefinition;
        }

        return typeDefinition;
    }

    private static int hashPrimitiveName(final String name) {
        if (name.length() < 3) {
            return 0;
        }
        return (name.charAt(0) + name.charAt(2)) % 16;
    }
}