/*
 * Copyright (C) 2014 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.PsiTreeUtil;

import java.util.Collections;
import java.util.List;

/**
 * Checks that subclasses of certain APIs are overriding all methods that were abstract
 * in one or more earlier API levels that are still targeted by the minSdkVersion
 * of this project.
 */
public class OverrideConcreteDetector extends Detector implements JavaPsiScanner {
    /** Are previously-abstract methods all overridden? */
    public static final Issue ISSUE = Issue.create(
        "OverrideAbstract", //$NON-NLS-1$
        "Not overriding abstract methods on older platforms",

        "To improve the usability of some APIs, some methods that used to be `abstract` have " +
        "been made concrete by adding default implementations. This means that when compiling " +
        "with new versions of the SDK, your code does not have to override these methods.\n" +
        "\n" +
        "However, if your code is also targeting older versions of the platform where these " +
        "methods were still `abstract`, the code will crash. You must override all methods " +
        "that used to be abstract in any versions targeted by your application's " +
        "`minSdkVersion`.",

        Category.CORRECTNESS,
        6,
        Severity.FATAL,
        new Implementation(
                OverrideConcreteDetector.class,
                Scope.JAVA_FILE_SCOPE)
    );

    // This check is currently hardcoded for the specific case of the
    // NotificationListenerService change in API 21. We should consider
    // attempting to infer this information automatically from changes in
    // the API current.txt file and making this detector more database driven,
    // like the API detector.

    private static final String NOTIFICATION_LISTENER_SERVICE_FQN
            = "android.service.notification.NotificationListenerService";
    public static final String STATUS_BAR_NOTIFICATION_FQN
            = "android.service.notification.StatusBarNotification";
    private static final String ON_NOTIFICATION_POSTED = "onNotificationPosted";
    private static final String ON_NOTIFICATION_REMOVED = "onNotificationRemoved";
    private static final int CONCRETE_IN = 21;

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

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

    @Nullable
    @Override
    public List<String> applicableSuperClasses() {
        return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN);
    }

    @Override
    public void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration) {
        JavaEvaluator evaluator = context.getEvaluator();
        if (evaluator.isAbstract(declaration)) {
            return;
        }

        int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(declaration));
        if (minSdk >= CONCRETE_IN) {
            return;
        }

        String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED};
        for (String methodName : methodNames) {
            boolean found = false;
            for (PsiMethod method : declaration.findMethodsByName(methodName, true)) {
                // Make sure it's not the base method, but that it's been defined
                // in a subclass, concretely
                PsiClass containingClass = method.getContainingClass();
                if (containingClass == null) {
                    continue;
                }
                if (NOTIFICATION_LISTENER_SERVICE_FQN.equals(containingClass.getQualifiedName())) {
                    continue;
                }
                // Make sure subclass isn't just defining another abstract definition
                // of the method
                if (evaluator.isAbstract(method)) {
                    continue;
                }
                // Make sure it has the exact right signature
                if (method.getParameterList().getParametersCount() != 1) {
                    continue; // Wrong signature
                }
                if (!evaluator.parameterHasType(method, 0, STATUS_BAR_NOTIFICATION_FQN)) {
                    continue;
                }

                found = true;
                break;
            }

            if (!found) {
                String message = String.format(
                        "Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d",
                        NOTIFICATION_LISTENER_SERVICE_FQN, methodName,
                        STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk);
                context.report(ISSUE, declaration, context.getNameLocation(declaration), message);
                break;
            }

        }
    }

    private static int getTargetApi(@NonNull PsiClass node) {
        while (node != null) {
            int targetApi = ApiDetector.getTargetApi(node.getModifierList());
            if (targetApi != -1) {
                return targetApi;
            }

            node = PsiTreeUtil.getParentOfType(node, PsiClass.class, true);
        }

        return -1;
    }
}
