/*
 * Copyright 2014 - Present Rafael Winterhalter
 *
 * 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.bytebuddy.asm;

import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.CompoundList;
import net.bytebuddy.utility.OpenedClassReader;
import net.bytebuddy.utility.nullability.AlwaysNull;
import net.bytebuddy.utility.nullability.MaybeNull;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import java.util.HashMap;
import java.util.Map;

import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;

/**
 * <p>
 * A visitor wrapper that removes fields or methods that match a given {@link ElementMatcher}.
 * </p>
 * <p>
 * <b>Important</b>: This matcher is not capable of removing synthetic bridge methods which will be retained if they are
 * declared by the same class. As bridge methods only invoke an overridden method, the dispatch should however not be
 * influenced by their retention.
 * </p>
 * <p>
 * <b>Important</b>: The removal of the method is not reflected in the created {@link net.bytebuddy.dynamic.DynamicType}'s
 * type description of the instrumented type.
 * </p>
 */
@HashCodeAndEqualsPlugin.Enhance
public class MemberRemoval extends AsmVisitorWrapper.AbstractBase {

    /**
     * The matcher that decides upon field removal.
     */
    private final ElementMatcher.Junction<FieldDescription.InDefinedShape> fieldMatcher;

    /**
     * The matcher that decides upon method removal.
     */
    private final ElementMatcher.Junction<MethodDescription> methodMatcher;

    /**
     * Creates a new member removal instance that does not specify the removal of any methods.
     */
    public MemberRemoval() {
        this(ElementMatchers.<FieldDescription.InDefinedShape>none(), ElementMatchers.<MethodDescription>none());
    }

    /**
     * Creates a new member removal instance.
     *
     * @param fieldMatcher  The matcher that decides upon field removal.
     * @param methodMatcher The matcher that decides upon field removal.
     */
    protected MemberRemoval(ElementMatcher.Junction<FieldDescription.InDefinedShape> fieldMatcher,
                            ElementMatcher.Junction<MethodDescription> methodMatcher) {
        this.fieldMatcher = fieldMatcher;
        this.methodMatcher = methodMatcher;
    }

    /**
     * Specifies that any field that matches the specified matcher should be removed.
     *
     * @param matcher The matcher that decides upon field removal.
     * @return A new member removal instance that removes all previously specified members and any fields that match the specified matcher.
     */
    public MemberRemoval stripFields(ElementMatcher<? super FieldDescription.InDefinedShape> matcher) {
        return new MemberRemoval(fieldMatcher.or(matcher), methodMatcher);
    }

    /**
     * Specifies that any method that matches the specified matcher should be removed. Note that this implementation will
     * not strip bridge methods for virtual overrides of generic methods.
     *
     * @param matcher The matcher that decides upon method removal.
     * @return A new member removal instance that removes all previously specified members and any method that matches the specified matcher.
     */
    public MemberRemoval stripMethods(ElementMatcher<? super MethodDescription> matcher) {
        return stripInvokables(isMethod().and(matcher));
    }

    /**
     * Specifies that any constructor that matches the specified matcher should be removed.
     *
     * @param matcher The matcher that decides upon constructor removal.
     * @return A new member removal instance that removes all previously specified members and any constructor that matches the specified matcher.
     */
    public MemberRemoval stripConstructors(ElementMatcher<? super MethodDescription> matcher) {
        return stripInvokables(isConstructor().and(matcher));
    }

    /**
     * Specifies that any method or constructor that matches the specified matcher should be removed. Note that this implementation will
     * not strip bridge methods for virtual overrides of generic methods.
     *
     * @param matcher The matcher that decides upon method and constructor removal.
     * @return A new member removal instance that removes all previously specified members and any method or constructor that matches the specified matcher.
     */
    public MemberRemoval stripInvokables(ElementMatcher<? super MethodDescription> matcher) {
        return new MemberRemoval(fieldMatcher, methodMatcher.or(matcher));
    }

    /**
     * {@inheritDoc}
     */
    public ClassVisitor wrap(TypeDescription instrumentedType,
                             ClassVisitor classVisitor,
                             Implementation.Context implementationContext,
                             TypePool typePool,
                             FieldList<FieldDescription.InDefinedShape> fields,
                             MethodList<?> methods,
                             int writerFlags,
                             int readerFlags) {
        Map<String, FieldDescription.InDefinedShape> mappedFields = new HashMap<String, FieldDescription.InDefinedShape>();
        for (FieldDescription.InDefinedShape fieldDescription : fields) {
            mappedFields.put(fieldDescription.getInternalName() + fieldDescription.getDescriptor(), fieldDescription);
        }
        Map<String, MethodDescription> mappedMethods = new HashMap<String, MethodDescription>();
        for (MethodDescription methodDescription : CompoundList.<MethodDescription>of(methods, new MethodDescription.Latent.TypeInitializer(instrumentedType))) {
            mappedMethods.put(methodDescription.getInternalName() + methodDescription.getDescriptor(), methodDescription);
        }
        return new MemberRemovingClassVisitor(classVisitor, fieldMatcher, methodMatcher, mappedFields, mappedMethods);
    }

    /**
     * A class visitor that removes members based on element matchers.
     */
    protected static class MemberRemovingClassVisitor extends ClassVisitor {

        /**
         * Indicates the removal of a field.
         */
        @javax.annotation.Nonnull(when = javax.annotation.meta.When.NEVER)
        private static final FieldVisitor REMOVE_FIELD = null;

        /**
         * Indicates the removal of a method.
         */
        @AlwaysNull
        private static final MethodVisitor REMOVE_METHOD = null;

        /**
         * The matcher that determines field removal.
         */
        private final ElementMatcher.Junction<FieldDescription.InDefinedShape> fieldMatcher;

        /**
         * The matcher that determines method removal.
         */
        private final ElementMatcher.Junction<MethodDescription> methodMatcher;

        /**
         * A mapping of field names and descriptors to their description.
         */
        private final Map<String, FieldDescription.InDefinedShape> fields;

        /**
         * A mapping of method names and descriptors to their description.
         */
        private final Map<String, MethodDescription> methods;

        /**
         * Creates a new member removing class visitor.
         *
         * @param classVisitor  The class visitor to delegate to.
         * @param fieldMatcher  The matcher that determines field removal.
         * @param methodMatcher The matcher that determines method removal.
         * @param fields        A mapping of field names and descriptors to their description.
         * @param methods       A mapping of method names and descriptors to their description.
         */
        protected MemberRemovingClassVisitor(ClassVisitor classVisitor,
                                             ElementMatcher.Junction<FieldDescription.InDefinedShape> fieldMatcher,
                                             ElementMatcher.Junction<MethodDescription> methodMatcher,
                                             Map<String, FieldDescription.InDefinedShape> fields,
                                             Map<String, MethodDescription> methods) {
            super(OpenedClassReader.ASM_API, classVisitor);
            this.fieldMatcher = fieldMatcher;
            this.methodMatcher = methodMatcher;
            this.fields = fields;
            this.methods = methods;
        }

        @Override
        @MaybeNull
        public FieldVisitor visitField(int modifiers, String internalName, String descriptor, @MaybeNull String signature, @MaybeNull Object value) {
            FieldDescription.InDefinedShape fieldDescription = fields.get(internalName + descriptor);
            return fieldDescription != null && fieldMatcher.matches(fieldDescription)
                    ? REMOVE_FIELD
                    : super.visitField(modifiers, internalName, descriptor, signature, value);
        }

        @Override
        @MaybeNull
        public MethodVisitor visitMethod(int modifiers, String internalName, String descriptor, @MaybeNull String signature, @MaybeNull String[] exception) {
            MethodDescription methodDescription = methods.get(internalName + descriptor);
            return methodDescription != null && methodMatcher.matches(methodDescription)
                    ? REMOVE_METHOD
                    : super.visitMethod(modifiers, internalName, descriptor, signature, exception);
        }
    }
}
