/*
 * Copyright (C) 2011 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.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_END;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_START;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.VIEW_MERGE;

import com.android.annotations.NonNull;
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.LintUtils;
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 com.android.tools.lint.detector.api.XmlContext;

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Checks whether the current node can be removed without affecting the layout.
 */
public class UselessViewDetector extends LayoutDetector {

    private static final Implementation IMPLEMENTATION = new Implementation(
            UselessViewDetector.class,
            Scope.RESOURCE_FILE_SCOPE);

    /** Issue of including a parent that has no value on its own */
    public static final Issue USELESS_PARENT = Issue.create(
            "UselessParent", //$NON-NLS-1$
            "Useless parent layout",
            "A layout with children that has no siblings, is not a scrollview or " +
            "a root layout, and does not have a background, can be removed and have " +
            "its children moved directly into the parent for a flatter and more " +
            "efficient layout hierarchy.",
            Category.PERFORMANCE,
            2,
            Severity.WARNING,
            IMPLEMENTATION);

    /** Issue of including a leaf that isn't shown */
    public static final Issue USELESS_LEAF = Issue.create(
            "UselessLeaf", //$NON-NLS-1$
            "Useless leaf layout",
            "A layout that has no children or no background can often be removed (since it " +
            "is invisible) for a flatter and more efficient layout hierarchy.",
            Category.PERFORMANCE,
            2,
            Severity.WARNING,
            IMPLEMENTATION);

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

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

    private static final List<String> CONTAINERS = new ArrayList<String>(18);
    static {
        CONTAINERS.add(ABSOLUTE_LAYOUT);
        CONTAINERS.add(FRAME_LAYOUT);
        CONTAINERS.add(GRID_LAYOUT);
        CONTAINERS.add(GRID_VIEW);
        CONTAINERS.add(HORIZONTAL_SCROLL_VIEW);
        CONTAINERS.add("ImageSwitcher");                      //$NON-NLS-1$
        CONTAINERS.add(LINEAR_LAYOUT);
        CONTAINERS.add(RADIO_GROUP);
        CONTAINERS.add(RELATIVE_LAYOUT);
        CONTAINERS.add(SCROLL_VIEW);
        CONTAINERS.add("SlidingDrawer");                      //$NON-NLS-1$
        CONTAINERS.add("StackView");                          //$NON-NLS-1$
        CONTAINERS.add(TABLE_LAYOUT);
        CONTAINERS.add(TABLE_ROW);
        CONTAINERS.add("TextSwitcher");                       //$NON-NLS-1$
        CONTAINERS.add("ViewAnimator");                       //$NON-NLS-1$
        CONTAINERS.add("ViewFlipper");                        //$NON-NLS-1$
        CONTAINERS.add("ViewSwitcher");                       //$NON-NLS-1$
        // Available ViewGroups that are not included by this check:
        //  CONTAINERS.add("android.gesture.GestureOverlayView");
        //  CONTAINERS.add("AdapterViewFlipper");
        //  CONTAINERS.add("DialerFilter");
        //  CONTAINERS.add("ExpandableListView");
        //  CONTAINERS.add("ListView");
        //  CONTAINERS.add("MediaController");
        //  CONTAINERS.add("merge");
        //  CONTAINERS.add("SearchView");
        //  CONTAINERS.add("TabWidget");
        //  CONTAINERS.add("TabHost");
    }
    @Override
    public Collection<String> getApplicableElements() {
        return CONTAINERS;
    }

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        int childCount = LintUtils.getChildCount(element);
        if (childCount == 0) {
            // Check to see if this is a leaf layout that can be removed
            checkUselessLeaf(context, element);
        } else {
            // Check to see if this is a middle-man layout which can be removed
            checkUselessMiddleLayout(context, element);
        }
    }

    // This is the old UselessLayoutCheck from layoutopt
    private static void checkUselessMiddleLayout(XmlContext context, Element element) {
        // Conditions:
        // - The node has children
        // - The node does not have siblings
        // - The node's parent is not a scroll view (horizontal or vertical)
        // - The node does not have a background or its parent does not have a
        //   background or neither the node and its parent have a background
        // - The parent is not a <merge/>

        Node parentNode = element.getParentNode();
        if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
            // Can't remove root
            return;
        }

        Element parent = (Element) parentNode;
        String parentTag = parent.getTagName();
        if (parentTag.equals(SCROLL_VIEW) || parentTag.equals(HORIZONTAL_SCROLL_VIEW) ||
                parentTag.equals(VIEW_MERGE)) {
            // Can't remove if the parent is a scroll view or a merge
            return;
        }

        // This method is only called when we've already ensured that it has children
        assert LintUtils.getChildCount(element) > 0;

        int parentChildCount = LintUtils.getChildCount(parent);
        if (parentChildCount != 1) {
            // Don't remove if the node has siblings
            return;
        }

        // - A parent can be removed if it doesn't have a background
        // - A parent can be removed if has a background *and* the child does not have a
        //   background (in which case, just move the background over to the child, remove
        //   the parent)
        // - If both child and parent have a background, the parent cannot be removed (a
        //   background can be translucent, have transparent padding, etc.)
        boolean nodeHasBackground = element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
        boolean parentHasBackground = parent.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
        if (nodeHasBackground && parentHasBackground) {
            // Can't remove because both define a background, and they might both be
            // visible (e.g. through transparency or padding).
            return;
        }

        // Certain parents are special - such as the TabHost and the GestureOverlayView -
        // where we want to leave things alone.
        if (!CONTAINERS.contains(parentTag)) {
            return;
        }

        if (element.hasAttribute(ATTR_STYLE) || parent.hasAttribute(ATTR_STYLE)) {
            // If there is a custom style on the parent, it might be setting
            // attributes like background, padding, etc.
            return;
        }

        // If we define a padding, and the parent provides a background, then
        // this view is not *necessarily* useless.
        if (parentHasBackground && (element.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_START)
                || element.hasAttributeNS(ANDROID_URI, ATTR_PADDING_END))) {
            return;
        }

        boolean hasId = element.hasAttributeNS(ANDROID_URI, ATTR_ID);
        Location location = context.getLocation(element);
        String tag = element.getTagName();
        String format;
        if (hasId) {
            format = "This `%1$s` layout or its `%2$s` parent is possibly useless";
        } else {
            format = "This `%1$s` layout or its `%2$s` parent is useless";
        }
        if (nodeHasBackground || parentHasBackground) {
            format += "; transfer the `background` attribute to the other view";
        }
        String message = String.format(format, tag, parentTag);
        context.report(USELESS_PARENT, element, location, message);
    }

    // This is the old UselessView check from layoutopt
    private static void checkUselessLeaf(XmlContext context, Element element) {
        assert LintUtils.getChildCount(element) == 0;

        // Conditions:
        // - The node is a container view (LinearLayout, etc.)
        // - The node has no id
        // - The node has no background
        // - The node has no children
        // - The node has no style
        // - The node is not a root

        if (element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
            return;
        }

        if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
            return;
        }

        if (element.hasAttribute(ATTR_STYLE)) {
            return;
        }

        if (element == context.document.getDocumentElement()) {
            return;
        }

        Location location = context.getLocation(element);
        String tag = element.getTagName();
        String message = String.format(
                "This `%1$s` view is useless (no children, no `background`, no `id`, no `style`)", tag);
        context.report(USELESS_LEAF, element, location, message);
    }
}
