/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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 com.android.tools.lint.checks;

import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.lint.checks.SupportAnnotationDetector.filterRelevantAnnotations;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;

import java.io.File;
import java.util.Collections;
import java.util.List;

import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Super;

/**
 * Makes sure that methods call super when overriding methods.
 */
public class CallSuperDetector extends Detector implements Detector.JavaScanner {
    private static final String CALL_SUPER_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CallSuper"; //$NON-NLS-1$
    private static final String ON_DETACHED_FROM_WINDOW = "onDetachedFromWindow";   //$NON-NLS-1$
    private static final String ON_VISIBILITY_CHANGED = "onVisibilityChanged";      //$NON-NLS-1$

    private static final Implementation IMPLEMENTATION = new Implementation(
            CallSuperDetector.class,
            Scope.JAVA_FILE_SCOPE);

    /** Missing call to super */
    public static final Issue ISSUE = Issue.create(
            "MissingSuperCall", //$NON-NLS-1$
            "Missing Super Call",

            "Some methods, such as `View#onDetachedFromWindow`, require that you also " +
            "call the super implementation as part of your method.",

            Category.CORRECTNESS,
            9,
            Severity.ERROR,
            IMPLEMENTATION);

    /** Constructs a new {@link CallSuperDetector} check */
    public CallSuperDetector() {
    }

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.FAST;
    }

    // ---- Implements JavaScanner ----

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Collections.<Class<? extends Node>>singletonList(MethodDeclaration.class);
    }

    @Override
    public AstVisitor createJavaVisitor(@NonNull final JavaContext context) {
        return new ForwardingAstVisitor() {
            @Override
            public boolean visitMethodDeclaration(MethodDeclaration node) {
                ResolvedNode resolved = context.resolve(node);
                if (resolved instanceof ResolvedMethod) {
                    ResolvedMethod method = (ResolvedMethod) resolved;
                    checkCallSuper(context, node, method);
                }

                return false;
            }
        };
    }

    private static void checkCallSuper(@NonNull JavaContext context,
            @NonNull MethodDeclaration declaration,
            @NonNull ResolvedMethod method) {

        ResolvedMethod superMethod = getRequiredSuperMethod(method);
        if (superMethod != null) {
            if (!SuperCallVisitor.callsSuper(context, declaration, superMethod)) {
                String methodName = method.getName();
                String message = "Overriding method should call `super."
                        + methodName + "`";
                Location location = context.getLocation(declaration.astMethodName());
                context.report(ISSUE, declaration, location, message);
            }
        }
    }

    /**
     * Checks whether the given method overrides a method which requires the super method
     * to be invoked, and if so, returns it (otherwise returns null)
     */
    @Nullable
    private static ResolvedMethod getRequiredSuperMethod(
            @NonNull ResolvedMethod method) {

        String name = method.getName();
        if (ON_DETACHED_FROM_WINDOW.equals(name)) {
            // No longer annotated on the framework method since it's
            // now handled via onDetachedFromWindowInternal, but overriding
            // is still dangerous if supporting older versions so flag
            // this for now (should make annotation carry metadata like
            // compileSdkVersion >= N).
            if (!method.getContainingClass().isSubclassOf(CLASS_VIEW, false)) {
                return null;
            }
            return method.getSuperMethod();
        } else if (ON_VISIBILITY_CHANGED.equals(name)) {
            // From Android Wear API; doesn't yet have an annotation
            // but we want to enforce this right away until the AAR
            // is updated to supply it once @CallSuper is available in
            // the support library
            if (!method.getContainingClass().isSubclassOf(
                    "android.support.wearable.watchface.WatchFaceService.Engine", false)) {
                return null;
            }
            return method.getSuperMethod();
        }

        // Look up annotations metadata
        ResolvedMethod directSuper = method.getSuperMethod();
        ResolvedMethod superMethod = directSuper;
        while (superMethod != null) {
            Iterable<ResolvedAnnotation> annotations = superMethod.getAnnotations();
            annotations = filterRelevantAnnotations(annotations);
            for (ResolvedAnnotation annotation : annotations) {
                String signature = annotation.getSignature();
                if (CALL_SUPER_ANNOTATION.equals(signature)) {
                    return directSuper;
                } else if (signature.endsWith(".OverrideMustInvoke")) {
                    // Handle findbugs annotation on the fly too
                    return directSuper;
                }
            }
            superMethod = superMethod.getSuperMethod();
        }

        return null;
    }

    /** Visits a method and determines whether the method calls its super method */
    private static class SuperCallVisitor extends ForwardingAstVisitor {
        private final JavaContext mContext;
        private final ResolvedMethod mMethod;
        private boolean mCallsSuper;

        public static boolean callsSuper(
                @NonNull JavaContext context,
                @NonNull MethodDeclaration methodDeclaration,
                @NonNull ResolvedMethod method) {
            SuperCallVisitor visitor = new SuperCallVisitor(context, method);
            methodDeclaration.accept(visitor);
            return visitor.mCallsSuper;
        }

        private SuperCallVisitor(@NonNull JavaContext context, @NonNull ResolvedMethod method) {
            mContext = context;
            mMethod = method;
        }

        @Override
        public boolean visitSuper(Super node) {
            ResolvedNode resolved = null;
            if (node.getParent() instanceof MethodInvocation) {
                resolved = mContext.resolve(node.getParent());
            }
            if (resolved == null) {
                resolved = mContext.resolve(node);
            }
            if (mMethod.equals(resolved)) {
                mCallsSuper = true;
                return true;
            }
            return false;
        }

        @Override
        public boolean visitNode(Node node) {
            return mCallsSuper || super.visitNode(node);
        }
    }
}
