/*
 * 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Maps;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiTypeCastExpression;

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

/**
 * Detector looking for casts on th result of context.getSystemService which are suspect
 */
public class ServiceCastDetector extends Detector implements JavaPsiScanner {
    /** The main issue discovered by this detector */
    public static final Issue ISSUE = Issue.create(
            "ServiceCast", //$NON-NLS-1$
            "Wrong system service casts",

            "When you call `Context#getSystemService()`, the result is typically cast to " +
            "a specific interface. This lint check ensures that the cast is compatible with " +
            "the expected type of the return value.",

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

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

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

    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("getSystemService"); //$NON-NLS-1$
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
            @NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
        PsiElement parent = LintUtils.skipParentheses(call.getParent());
        if (parent instanceof PsiTypeCastExpression) {
            PsiTypeCastExpression cast = (PsiTypeCastExpression) parent;

            PsiExpression[] args = call.getArgumentList().getExpressions();
            if (args.length == 1 && args[0] instanceof PsiReferenceExpression) {
                String name = ((PsiReferenceExpression)args[0]).getReferenceName();
                String expectedClass = getExpectedType(name);
                if (expectedClass != null && cast.getCastType() != null) {
                    String castType = cast.getCastType().getType().getCanonicalText();
                    if (castType.indexOf('.') == -1) {
                        expectedClass = stripPackage(expectedClass);
                    }
                    if (!castType.equals(expectedClass)) {
                        // It's okay to mix and match
                        // android.content.ClipboardManager and android.text.ClipboardManager
                        if (isClipboard(castType) && isClipboard(expectedClass)) {
                            return;
                        }

                        String message = String.format(
                                "Suspicious cast to `%1$s` for a `%2$s`: expected `%3$s`",
                                stripPackage(castType), name, stripPackage(expectedClass));
                        context.report(ISSUE, call, context.getLocation(cast), message);
                    }
                }

            }
        }
    }

    private static boolean isClipboard(String cls) {
        return cls.equals("android.content.ClipboardManager")      //$NON-NLS-1$
                || cls.equals("android.text.ClipboardManager");    //$NON-NLS-1$
    }

    private static String stripPackage(String fqcn) {
        int index = fqcn.lastIndexOf('.');
        if (index != -1) {
            fqcn = fqcn.substring(index + 1);
        }

        return fqcn;
    }

    @Nullable
    private static String getExpectedType(@Nullable String value) {
        return value != null ? getServiceMap().get(value) : null;
    }

    @NonNull
    private static Map<String, String> getServiceMap() {
        if (sServiceMap == null) {
            final int EXPECTED_SIZE = 55;
            sServiceMap = Maps.newHashMapWithExpectedSize(EXPECTED_SIZE);

            sServiceMap.put("ACCESSIBILITY_SERVICE", "android.view.accessibility.AccessibilityManager");
            sServiceMap.put("ACCOUNT_SERVICE", "android.accounts.AccountManager");
            sServiceMap.put("ACTIVITY_SERVICE", "android.app.ActivityManager");
            sServiceMap.put("ALARM_SERVICE", "android.app.AlarmManager");
            sServiceMap.put("APPWIDGET_SERVICE", "android.appwidget.AppWidgetManager");
            sServiceMap.put("APP_OPS_SERVICE", "android.app.AppOpsManager");
            sServiceMap.put("AUDIO_SERVICE", "android.media.AudioManager");
            sServiceMap.put("BATTERY_SERVICE", "android.os.BatteryManager");
            sServiceMap.put("BLUETOOTH_SERVICE", "android.bluetooth.BluetoothManager");
            sServiceMap.put("CAMERA_SERVICE", "android.hardware.camera2.CameraManager");
            sServiceMap.put("CAPTIONING_SERVICE", "android.view.accessibility.CaptioningManager");
            sServiceMap.put("CARRIER_CONFIG_SERVICE", "android.telephony.CarrierConfigManager");
            sServiceMap.put("CLIPBOARD_SERVICE", "android.text.ClipboardManager"); // also allow @Deprecated android.content.ClipboardManager
            sServiceMap.put("CONNECTIVITY_SERVICE", "android.net.ConnectivityManager");
            sServiceMap.put("CONSUMER_IR_SERVICE", "android.hardware.ConsumerIrManager");
            sServiceMap.put("DEVICE_POLICY_SERVICE", "android.app.admin.DevicePolicyManager");
            sServiceMap.put("DISPLAY_SERVICE", "android.hardware.display.DisplayManager");
            sServiceMap.put("DOWNLOAD_SERVICE", "android.app.DownloadManager");
            sServiceMap.put("DROPBOX_SERVICE", "android.os.DropBoxManager");
            sServiceMap.put("FINGERPRINT_SERVICE", "android.hardware.fingerprint.FingerprintManager");
            sServiceMap.put("INPUT_METHOD_SERVICE", "android.view.inputmethod.InputMethodManager");
            sServiceMap.put("INPUT_SERVICE", "android.hardware.input.InputManager");
            sServiceMap.put("JOB_SCHEDULER_SERVICE", "android.app.job.JobScheduler");
            sServiceMap.put("KEYGUARD_SERVICE", "android.app.KeyguardManager");
            sServiceMap.put("LAUNCHER_APPS_SERVICE", "android.content.pm.LauncherApps");
            sServiceMap.put("LAYOUT_INFLATER_SERVICE", "android.view.LayoutInflater");
            sServiceMap.put("LOCATION_SERVICE", "android.location.LocationManager");
            sServiceMap.put("MEDIA_PROJECTION_SERVICE", "android.media.projection.MediaProjectionManager");
            sServiceMap.put("MEDIA_ROUTER_SERVICE", "android.media.MediaRouter");
            sServiceMap.put("MEDIA_SESSION_SERVICE", "android.media.session.MediaSessionManager");
            sServiceMap.put("MIDI_SERVICE", "android.media.midi.MidiManager");
            sServiceMap.put("NETWORK_STATS_SERVICE", "android.app.usage.NetworkStatsManager");
            sServiceMap.put("NFC_SERVICE", "android.nfc.NfcManager");
            sServiceMap.put("NOTIFICATION_SERVICE", "android.app.NotificationManager");
            sServiceMap.put("NSD_SERVICE", "android.net.nsd.NsdManager");
            sServiceMap.put("POWER_SERVICE", "android.os.PowerManager");
            sServiceMap.put("PRINT_SERVICE", "android.print.PrintManager");
            sServiceMap.put("RESTRICTIONS_SERVICE", "android.content.RestrictionsManager");
            sServiceMap.put("SEARCH_SERVICE", "android.app.SearchManager");
            sServiceMap.put("SENSOR_SERVICE", "android.hardware.SensorManager");
            sServiceMap.put("STORAGE_SERVICE", "android.os.storage.StorageManager");
            sServiceMap.put("TELECOM_SERVICE", "android.telecom.TelecomManager");
            sServiceMap.put("TELEPHONY_SERVICE", "android.telephony.TelephonyManager");
            sServiceMap.put("TELEPHONY_SUBSCRIPTION_SERVICE", "android.telephony.SubscriptionManager");
            sServiceMap.put("TEXT_SERVICES_MANAGER_SERVICE", "android.view.textservice.TextServicesManager");
            sServiceMap.put("TV_INPUT_SERVICE", "android.media.tv.TvInputManager");
            sServiceMap.put("UI_MODE_SERVICE", "android.app.UiModeManager");
            sServiceMap.put("USAGE_STATS_SERVICE", "android.app.usage.UsageStatsManager");
            sServiceMap.put("USB_SERVICE", "android.hardware.usb.UsbManager");
            sServiceMap.put("USER_SERVICE", "android.os.UserManager");
            sServiceMap.put("VIBRATOR_SERVICE", "android.os.Vibrator");
            sServiceMap.put("WALLPAPER_SERVICE", "android.service.wallpaper.WallpaperService");
            sServiceMap.put("WIFI_P2P_SERVICE", "android.net.wifi.p2p.WifiP2pManager");
            sServiceMap.put("WIFI_SERVICE", "android.net.wifi.WifiManager");
            sServiceMap.put("WINDOW_SERVICE", "android.view.WindowManager");

            assert sServiceMap.size() == EXPECTED_SIZE : sServiceMap.size();
        }

        return sServiceMap;
    }

    private static Map<String, String> sServiceMap;
}
