/*
 * Copyright (C) 2015 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.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ICON;
import static com.android.SdkConstants.ATTR_LABEL;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_THEME;
import static com.android.SdkConstants.ATTR_TYPE;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.utils.SdkUtils.endsWithIgnoreCase;
import static com.android.xml.AndroidManifest.NODE_METADATA;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.common.resources.configuration.VersionQualifier;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

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

/**
 * Detects references to resources in the manifest that vary by configuration
 */
public class ManifestResourceDetector extends ResourceXmlDetector {
    /** Using resources in the manifest that vary by configuration */
    @SuppressWarnings("unchecked")
    public static final Issue ISSUE = Issue.create(
            "ManifestResource", //$NON-NLS-1$
            "Manifest Resource References",
            "Elements in the manifest can reference resources, but those resources cannot " +
            "vary across configurations (except as a special case, by version, and except " +
            "for a few specific package attributes such as the application title and icon.)",

            Category.CORRECTNESS,
            6,
            Severity.FATAL,
            new Implementation(
                    ManifestResourceDetector.class,
                    Scope.MANIFEST_AND_RESOURCE_SCOPE,
                    Scope.MANIFEST_SCOPE));

    /**
     * Map from resource name to resource type to manifest location; used
     * in batch mode to report errors when resource overrides are found
     */
    private Map<String, Multimap<ResourceType, Location>> mManifestLocations;

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

    @Override
    public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
        if (endsWithIgnoreCase(context.file.getPath(), ANDROID_MANIFEST_XML)) {
            checkManifest(context, document);
        } else {
            //noinspection VariableNotUsedInsideIf
            if (mManifestLocations != null) {
                checkResourceFile(context, document);
            }
        }
    }

    private void checkManifest(@NonNull XmlContext context, @NonNull Document document) {
        LintClient client = context.getClient();
        Project project = context.getProject();
        AbstractResourceRepository repository = null;
        if (client.supportsProjectResources()) {
            repository = client.getResourceRepository(project, true, false);
        }
        if (repository == null && !context.getScope().contains(Scope.RESOURCE_FILE)) {
            // Can't perform incremental analysis without a resource repository
            return;
        }

        Element root = document.getDocumentElement();
        if (root != null) {
            visit(context, root, repository);
        }
    }

    private void visit(@NonNull XmlContext context, @NonNull Element element,
            @Nullable AbstractResourceRepository repository) {
        if (NODE_METADATA.equals(element.getTagName())) {
            return;
        }

        NamedNodeMap attributes = element.getAttributes();
        for (int i = 0, n = attributes.getLength(); i < n; i++) {
            Node node = attributes.item(i);
            String value = node.getNodeValue();
            if (value.startsWith(PREFIX_RESOURCE_REF)) {
                Attr attribute = (Attr) node;
                if (!isAllowedToVary(attribute)) {
                    checkReference(context, attribute, value, repository);
                }
            }
        }

        NodeList children = element.getChildNodes();
        for (int i = 0, n = children.getLength(); i < n; i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                visit(context, ((Element)child), repository);
            }
        }
    }

    /**
     * Is the given attribute allowed to reference a resource that has different
     * values across configurations (other than with version qualifiers) ?
     * <p>
     * When the manifest is read, it has a fixed configuration with only the API level set.
     * When strings are read, we can either read the actual string, or a resource reference.
     * For labels and icons, we only read the resource reference -- that is the package manager
     * doesn't need the actual string (like it would need for, say, the name of an activity),
     * but just gets the resource ID, and then clients if they need the actual resource value can
     * load it at that point using their current configuration.
     * <p>
     * To see which specific attributes in the manifest are processed this way, look at
     * android.content.pm.PackageItemInfo to see what pieces of data are kept as raw resource
     * IDs instead of loading their value. (For label resources we also keep the non localized
     * label resource to allow people to specify hardcoded strings instead of a resource reference.)
     *
     * @param attribute the attribute node to look up
     * @return true if this resource is allowed to have delayed configuration values
     */
    private static boolean isAllowedToVary(@NonNull Attr attribute) {
        // This corresponds to the getResourceId() calls in PackageParser
        // where we store the actual resource id such that they can be
        // resolved later
        String name = attribute.getLocalName();
        if (ATTR_LABEL.equals(name)
                || ATTR_ICON.equals(name)
                || ATTR_THEME.equals(name)
                || "description".equals(name)
                || "logo".equals(name)
                || "banner".equals(name)
                || "sharedUserLabel".equals(name)) {
            return ANDROID_URI.equals(attribute.getNamespaceURI());
        }

        return false;
    }

    private void checkReference(
            @NonNull XmlContext context,
            @NonNull Attr attribute,
            @NonNull String value,
            @Nullable AbstractResourceRepository repository) {
        ResourceUrl url = ResourceUrl.parse(value);
        if (url != null && !url.framework) {
            if (repository != null) {
                List<ResourceItem> items = repository.getResourceItem(url.type, url.name);
                if (items != null && items.size() > 1) {
                    List<String> list = Lists.newArrayListWithExpectedSize(5);
                    for (ResourceItem item : items) {
                        String qualifiers = item.getQualifiers();
                        // Default folder is okay
                        if (qualifiers.isEmpty()) {
                            continue;
                        }

                        // Version qualifier is okay
                        if (VersionQualifier.getQualifier(qualifiers) != null) {
                            continue;
                        }

                        list.add(qualifiers);
                    }
                    if (!list.isEmpty()) {
                        Collections.sort(list);
                        String message = getErrorMessage(Joiner.on(", ").join(list));
                        context.report(ISSUE, attribute, context.getValueLocation(attribute),
                                message);
                    }
                }
            } else if (!context.getDriver().isSuppressed(context, ISSUE, attribute)) {
                // Don't have a resource repository; need to check resource files during batch
                // run
                if (mManifestLocations == null) {
                    mManifestLocations = Maps.newHashMap();
                }
                Multimap<ResourceType, Location> typeMap = mManifestLocations.get(url.name);
                if (typeMap == null) {
                    typeMap = ArrayListMultimap.create();
                    mManifestLocations.put(url.name, typeMap);
                }
                typeMap.put(url.type, context.getValueLocation(attribute));
            }
        }
    }

    private void checkResourceFile(
            @NonNull XmlContext context,
            @NonNull Document document) {
        File parentFile = context.file.getParentFile();
        if (parentFile == null) {
            return;
        }
        String parentName = parentFile.getName();
        // Base folders are okay
        int index = parentName.indexOf('-');
        if (index == -1) {
            return;
        }

        // Version qualifier is okay
        String qualifiers = parentName.substring(index + 1);
        if (VersionQualifier.getQualifier(qualifiers) != null) {
            return;
        }

        ResourceFolderType folderType = context.getResourceFolderType();
        if (folderType == ResourceFolderType.VALUES) {
            Element root = document.getDocumentElement();
            if (root != null) {
                NodeList children = root.getChildNodes();
                for (int i = 0, n = children.getLength(); i < n; i++) {
                    Node child = children.item(i);
                    if (child.getNodeType() == Node.ELEMENT_NODE) {
                        Element item = (Element)child;
                        String name = item.getAttribute(ATTR_NAME);
                        if (name != null && mManifestLocations.containsKey(name)) {
                            String tag = item.getTagName();
                            String typeString = tag;
                            if (tag.equals(TAG_ITEM)) {
                                typeString = item.getAttribute(ATTR_TYPE);
                            }
                            ResourceType type = ResourceType.getEnum(typeString);
                            if (type != null) {
                                reportIfFound(context, qualifiers, name, type, item);
                            }
                        }
                    }
                }

            }
        } else if (folderType != null) {
            String name = LintUtils.getBaseName(context.file.getName());
            if (mManifestLocations.containsKey(name)) {
                List<ResourceType> types =
                        FolderTypeRelationship.getRelatedResourceTypes(folderType);
                for (ResourceType type : types) {
                    reportIfFound(context, qualifiers, name, type, document.getDocumentElement());
                }
            }
        }
    }

    private void reportIfFound(@NonNull XmlContext context, @NonNull  String qualifiers,
            @NonNull  String name, @NonNull  ResourceType type, @Nullable Node secondary) {
        Multimap<ResourceType, Location> typeMap = mManifestLocations.get(name);
        if (typeMap != null) {
            Collection<Location> locations = typeMap.get(type);
            if (locations != null) {
                for (Location location : locations) {
                    String message = getErrorMessage(qualifiers);
                    if (secondary != null) {
                        Location secondaryLocation = context.getLocation(secondary);
                        secondaryLocation.setSecondary(location.getSecondary());
                        secondaryLocation.setMessage("This value will not be used");
                        location.setSecondary(secondaryLocation);
                    }
                    context.report(ISSUE, location, message);
                }
            }
        }
    }

    @NonNull
    private static String getErrorMessage(@NonNull  String qualifiers) {
        return "Resources referenced from the manifest cannot vary by configuration "
                + "(except for version qualifiers, e.g. `-v21`.) Found variation in " + qualifiers;
    }
}
