/*
 * 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 static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.DIMEN_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.TAG_DIMEN;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
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.LayoutDetector;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
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 com.android.tools.lint.detector.api.XmlContext;

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

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

/**
 * Checks for negative margins in the following scenarios:
 * <ul>
 *     <li>In direct layout attribute usages, e.g. {@code <Button android:layoutMargin="-5dp"}</li>
 *     <li>In theme styles, e.g. {@code <item name="android:layoutMargin">-5dp</item>}</li>
 *     <li>In dimension usages, e.g. {@code <Button android:layoutMargin="@dimen/foo"} along
 *      with {@code <dimen name="foo">-5dp</dimen>}</li>
 * </ul>
 */
public class NegativeMarginDetector extends LayoutDetector {
    private static final Implementation IMPLEMENTATION = new Implementation(
            NegativeMarginDetector.class,
            Scope.RESOURCE_FILE_SCOPE);

    /** Negative margins */
    public static final Issue ISSUE = Issue.create(
            "NegativeMargin", //$NON-NLS-1$
            "Negative Margins",

            "Margin values should be positive. Negative values are generally a sign that " +
            "you are making assumptions about views surrounding the current one, or may be "+
            "tempted to turn off child clipping to allow a view to escape its parent. " +
            "Turning off child clipping to do this not only leads to poor graphical " +
            "performance, it also results in wrong touch event handling since touch events " +
            "are based strictly on a chain of parent-rect hit tests. Finally, making " +
            "assumptions about the size of strings can lead to localization problems.",
            Category.USABILITY,
            4,
            Severity.WARNING,
            IMPLEMENTATION).setEnabledByDefault(false);

    private HashMap<String, Location.Handle> mDimenUsage;


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

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

    @Override
    public boolean appliesTo(@NonNull ResourceFolderType folderType) {
        // Look in both layouts (at attribute values) and in value files (style and dimension
        // definitions)
        return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
    }

    @Override
    public Collection<String> getApplicableAttributes() {
        return Arrays.asList(
                ATTR_LAYOUT_MARGIN,
                ATTR_LAYOUT_MARGIN_LEFT,
                ATTR_LAYOUT_MARGIN_TOP,
                ATTR_LAYOUT_MARGIN_RIGHT,
                ATTR_LAYOUT_MARGIN_BOTTOM,
                ATTR_LAYOUT_MARGIN_START,
                ATTR_LAYOUT_MARGIN_END
        );
    }

    @Override
    @Nullable
    public Collection<String> getApplicableElements() {
        return Arrays.asList(TAG_DIMEN, TAG_STYLE);
    }

    @Override
    public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
        String value = attribute.getValue();
        checkMarginValue(context, value, attribute, null);
    }

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        if (context.getResourceFolderType() != ResourceFolderType.VALUES) {
            return;
        }

        String tag = element.getTagName();
        if (TAG_DIMEN.equals(tag)) {
            NodeList itemNodes = element.getChildNodes();
            String name = element.getAttribute(ATTR_NAME);
            Location.Handle handle = mDimenUsage != null ? mDimenUsage.get(name) : null;
            if (handle != null) {
                for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
                    Node item = itemNodes.item(j);
                    if (item.getNodeType() == Node.TEXT_NODE) {
                        String text = item.getNodeValue().trim();
                        checkMarginValue(context, text, null, handle);
                    }
                }
            }
        } else {
            assert TAG_STYLE.equals(tag) : tag;
            NodeList itemNodes = element.getChildNodes();
            for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
                Node item = itemNodes.item(j);
                if (item.getNodeType() == Node.ELEMENT_NODE &&
                        TAG_ITEM.equals(item.getNodeName())) {
                    Element itemElement = (Element) item;
                    String name = itemElement.getAttribute(ATTR_NAME);
                    if (name.startsWith(PREFIX_ANDROID) &&
                            name.startsWith(ATTR_LAYOUT_MARGIN, PREFIX_ANDROID.length())) {
                        NodeList childNodes = item.getChildNodes();
                        for (int i = 0, n = childNodes.getLength(); i < n; i++) {
                            Node child = childNodes.item(i);
                            if (child.getNodeType() != Node.TEXT_NODE) {
                                return;
                            }

                            checkMarginValue(context, child.getNodeValue(), child, null);
                        }
                    }
                }

            }
        }
    }

    private static boolean isNegativeDimension(@NonNull String value) {
        return value.trim().startsWith("-");
    }

    private void checkMarginValue(
            @NonNull XmlContext context,
            @NonNull String value,
            @Nullable Node scope,
            @Nullable Location.Handle handle) {
        if (isNegativeDimension(value)) {
            String message = "Margin values should not be negative";
            if (scope != null) {
                context.report(ISSUE, scope, context.getLocation(scope), message);
            } else {
                assert handle != null;
                context.report(ISSUE, handle.resolve(), message);
            }
        } else if (value.startsWith(DIMEN_PREFIX) && scope != null) {
            ResourceUrl url = ResourceUrl.parse(value);
            if (url == null) {
                return;
            }
            if (context.getClient().supportsProjectResources()) {
                // Typically interactive IDE usage, where we are only analyzing a single file,
                // but we can use the IDE to resolve resource URLs
                LintClient client = context.getClient();
                Project project = context.getProject();
                AbstractResourceRepository resources = client.getResourceRepository(project, true,
                    true);
                if (resources != null) {
                    List<ResourceItem> items = resources.getResourceItem(url.type, url.name);
                    if (items != null) {
                        for (ResourceItem item : items) {
                            ResourceValue resourceValue = item.getResourceValue(false);
                            if (resourceValue != null) {
                                String dimenValue = resourceValue.getValue();
                                if (dimenValue != null && isNegativeDimension(dimenValue)) {
                                    ResourceFile sourceFile = item.getSource();
                                    assert sourceFile != null;
                                    String message = String.format(
                                            "Margin values should not be negative "
                                                    + "(`%1$s` is defined as `%2$s` in `%3$s`",
                                            value, dimenValue, sourceFile.getFile());
                                    context.report(ISSUE, scope,
                                            context.getLocation(scope),
                                            message);
                                    break;
                                }
                            }
                        }
                    }
                }
            } else if (!context.getDriver().isSuppressed(context, ISSUE, scope)) {
                // Batch mode where we process layouts then values in order
                if (mDimenUsage == null) {
                    mDimenUsage = new HashMap<String, Location.Handle>();
                }
                mDimenUsage.put(url.name, context.createLocationHandle(scope));
            }
        }
    }
}
