/*
 * Copyright (C) 2012 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_STRING_PREFIX;
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_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_ORIENTATION;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.STRING_PREFIX;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.VALUE_SELECTABLE_ITEM_BACKGROUND;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.SdkConstants.VALUE_VERTICAL;
import static com.android.tools.lint.checks.RequiredAttributeDetector.PERCENT_RELATIVE_LAYOUT;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
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.ResourceXmlDetector;
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.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Check which looks at the order of buttons in dialogs and makes sure that
 * "the dismissive action of a dialog is always on the left whereas the affirmative actions
 * are on the right."
 * <p>
 * This only looks for the affirmative and dismissive actions named "OK" and "Cancel";
 * "Cancel" usually works, but the affirmative action often has many other names -- "Done",
 * "Send", "Go", etc.
 * <p>
 * TODO: Perhaps we should look for Yes/No dialogs and suggested they be rephrased as
 * Cancel/OK dialogs? Similarly, consider "Abort" a synonym for "Cancel" ?
 */
public class ButtonDetector extends ResourceXmlDetector {
    /** Name of cancel value ("Cancel") */
    private static final String CANCEL_LABEL = "Cancel";
    /** Name of OK value ("Cancel") */
    private static final String OK_LABEL = "OK";
    /** Name of Back value ("Back") */
    private static final String BACK_LABEL = "Back";
    /** Yes */
    private static final String YES_LABEL = "Yes";
    /** No */
    private static final String NO_LABEL = "No";

    /** Layout text attribute reference to {@code @android:string/ok} */
    private static final String ANDROID_OK_RESOURCE =
            ANDROID_STRING_PREFIX + "ok"; //$NON-NLS-1$
    /** Layout text attribute reference to {@code @android:string/cancel} */
    private static final String ANDROID_CANCEL_RESOURCE =
            ANDROID_STRING_PREFIX + "cancel"; //$NON-NLS-1$
    /** Layout text attribute reference to {@code @android:string/yes} */
    private static final String ANDROID_YES_RESOURCE =
            ANDROID_STRING_PREFIX + "yes"; //$NON-NLS-1$
    /** Layout text attribute reference to {@code @android:string/no} */
    private static final String ANDROID_NO_RESOURCE =
            ANDROID_STRING_PREFIX + "no"; //$NON-NLS-1$

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

    /** The main issue discovered by this detector */
    public static final Issue ORDER = Issue.create(
            "ButtonOrder", //$NON-NLS-1$
            "Button order",

            "According to the Android Design Guide,\n" +
            "\n" +
            "\"Action buttons are typically Cancel and/or OK, with OK indicating the preferred " +
            "or most likely action. However, if the options consist of specific actions such " +
            "as Close or Wait rather than a confirmation or cancellation of the action " +
            "described in the content, then all the buttons should be active verbs. As a rule, " +
            "the dismissive action of a dialog is always on the left whereas the affirmative " +
            "actions are on the right.\"\n" +
            "\n" +
            "This check looks for button bars and buttons which look like cancel buttons, " +
            "and makes sure that these are on the left.",

            Category.USABILITY,
            8,
            Severity.WARNING,
            IMPLEMENTATION)
            .addMoreInfo(
                    "http://developer.android.com/design/building-blocks/dialogs.html"); //$NON-NLS-1$

    /** The main issue discovered by this detector */
    public static final Issue STYLE = Issue.create(
            "ButtonStyle", //$NON-NLS-1$
            "Button should be borderless",

            "Button bars typically use a borderless style for the buttons. Set the " +
            "`style=\"?android:attr/buttonBarButtonStyle\"` attribute " +
            "on each of the buttons, and set `style=\"?android:attr/buttonBarStyle\"` on " +
            "the parent layout",

            Category.USABILITY,
            5,
            Severity.WARNING,
            IMPLEMENTATION)
            .addMoreInfo(
                    "http://developer.android.com/design/building-blocks/buttons.html"); //$NON-NLS-1$

    /** The main issue discovered by this detector */
    public static final Issue BACK_BUTTON = Issue.create(
            "BackButton", //$NON-NLS-1$
            "Back button",
            // TODO: Look for ">" as label suffixes as well

            "According to the Android Design Guide,\n" +
            "\n" +
            "\"Other platforms use an explicit back button with label to allow the user " +
            "to navigate up the application's hierarchy. Instead, Android uses the main " +
            "action bar's app icon for hierarchical navigation and the navigation bar's " +
            "back button for temporal navigation.\"" +
            "\n" +
            "This check is not very sophisticated (it just looks for buttons with the " +
            "label \"Back\"), so it is disabled by default to not trigger on common " +
            "scenarios like pairs of Back/Next buttons to paginate through screens.",

            Category.USABILITY,
            6,
            Severity.WARNING,
            IMPLEMENTATION)
            .setEnabledByDefault(false)
            .addMoreInfo(
                 "http://developer.android.com/design/patterns/pure-android.html"); //$NON-NLS-1$

    /** The main issue discovered by this detector */
    public static final Issue CASE = Issue.create(
            "ButtonCase", //$NON-NLS-1$
            "Cancel/OK dialog button capitalization",

            "The standard capitalization for OK/Cancel dialogs is \"OK\" and \"Cancel\". " +
            "To ensure that your dialogs use the standard strings, you can use " +
            "the resource strings @android:string/ok and @android:string/cancel.",

            Category.USABILITY,
            2,
            Severity.WARNING,
            IMPLEMENTATION);

    /** Set of resource names whose value was either OK or Cancel */
    private Set<String> mApplicableResources;

    /**
     * Map of resource names we'd like resolved into strings in phase 2. The
     * values should be filled in with the actual string contents.
     */
    private Map<String, String> mKeyToLabel;

    /**
     * Set of elements we've already warned about. If we've already complained
     * about a cancel button, don't also report the OK button (since it's listed
     * for the warnings on OK buttons).
     */
    private Set<Element> mIgnore;

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

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

    @Override
    public Collection<String> getApplicableElements() {
        return Arrays.asList(BUTTON, TAG_STRING);
    }

    @Override
    public boolean appliesTo(@NonNull ResourceFolderType folderType) {
        return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        int phase = context.getPhase();
        if (phase == 1 && mApplicableResources != null) {
            // We found resources for the string "Cancel"; perform a second pass
            // where we check layout text attributes against these strings.
            context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
        }
    }

    private static String stripLabel(String text) {
        text = text.trim();
        if (text.length() > 2
                && (text.charAt(0) == '"' || text.charAt(0) == '\'')
                && (text.charAt(0) == text.charAt(text.length() - 1))) {
            text = text.substring(1, text.length() - 1);
        }

        return text;
    }

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        // This detector works in two passes.
        // In pass 1, it looks in layout files for hardcoded strings of "Cancel", or
        // references to @string/cancel or @android:string/cancel.
        // It also looks in values/ files for strings whose value is "Cancel",
        // and if found, stores the corresponding keys in a map. (This is necessary
        // since value files are processed after layout files).
        // Then, if at the end of phase 1 any "Cancel" string resources were
        // found in the value files, then it requests a *second* phase,
        // where it looks only for <Button>'s whose text matches one of the
        // cancel string resources.
        int phase = context.getPhase();
        String tagName = element.getTagName();
        if (phase == 1 && tagName.equals(TAG_STRING)) {
            NodeList childNodes = element.getChildNodes();
            for (int i = 0, n = childNodes.getLength(); i < n; i++) {
                Node child = childNodes.item(i);
                if (child.getNodeType() == Node.TEXT_NODE) {
                    String text = child.getNodeValue();
                    for (int j = 0, len = text.length(); j < len; j++) {
                        char c = text.charAt(j);
                        if (!Character.isWhitespace(c)) {
                            if (c == '"' || c == '\'') {
                                continue;
                            }
                            if (LintUtils.startsWith(text, CANCEL_LABEL, j)) {
                                String label = stripLabel(text);
                                if (label.equalsIgnoreCase(CANCEL_LABEL)) {
                                    String name = element.getAttribute(ATTR_NAME);
                                    foundResource(context, name, element);

                                    if (!label.equals(CANCEL_LABEL)
                                            && LintUtils.isEnglishResource(context, true)
                                            && context.isEnabled(CASE)) {
                                        assert label.trim().equalsIgnoreCase(CANCEL_LABEL) : label;
                                        context.report(CASE, child, context.getLocation(child),
                                            String.format(
                                            "The standard Android way to capitalize %1$s " +
                                            "is \"Cancel\" (tip: use `@android:string/cancel` instead)",
                                            label));
                                    }
                                }
                            } else if (LintUtils.startsWith(text, OK_LABEL, j)) {
                                String label = stripLabel(text);
                                if (label.equalsIgnoreCase(OK_LABEL)) {
                                    String name = element.getAttribute(ATTR_NAME);
                                    foundResource(context, name, element);

                                    if (!label.equals(OK_LABEL)
                                            && LintUtils.isEnglishResource(context, true)
                                            && context.isEnabled(CASE)) {
                                        assert label.trim().equalsIgnoreCase(OK_LABEL) : label;
                                        context.report(CASE, child, context.getLocation(child),
                                            String.format(
                                            "The standard Android way to capitalize %1$s " +
                                            "is \"OK\" (tip: use `@android:string/ok` instead)",
                                            label));
                                    }
                                }
                            } else if (LintUtils.startsWith(text, BACK_LABEL, j) &&
                                    stripLabel(text).equalsIgnoreCase(BACK_LABEL)) {
                                String name = element.getAttribute(ATTR_NAME);
                                foundResource(context, name, element);
                            }
                            break;
                        }
                    }
                }
            }
        } else if (tagName.equals(BUTTON)) {
            if (phase == 1) {
                if (isInButtonBar(element)
                        && !element.hasAttribute(ATTR_STYLE)
                        && !VALUE_SELECTABLE_ITEM_BACKGROUND.equals(
                                element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND))
                        && (context.getProject().getMinSdk() >= 11
                            || context.getFolderVersion() >= 11)
                        && context.isEnabled(STYLE)
                        && !parentDefinesSelectableItem(element)) {
                    context.report(STYLE, element, context.getLocation(element),
                            "Buttons in button bars should be borderless; use " +
                            "`style=\"?android:attr/buttonBarButtonStyle\"` (and " +
                            "`?android:attr/buttonBarStyle` on the parent)");
                }
            }

            String text = element.getAttributeNS(ANDROID_URI, ATTR_TEXT);
            if (phase == 2) {
                if (mApplicableResources.contains(text)) {
                    String key = text;
                    if (key.startsWith(STRING_PREFIX)) {
                        key = key.substring(STRING_PREFIX.length());
                    }
                    String label = mKeyToLabel.get(key);
                    boolean isCancel = CANCEL_LABEL.equalsIgnoreCase(label);
                    if (isCancel) {
                        if (isWrongCancelPosition(element)) {
                            reportCancelPosition(context, element);
                        }
                    } else if (OK_LABEL.equalsIgnoreCase(label)) {
                        if (isWrongOkPosition(element)) {
                            reportOkPosition(context, element);
                        }
                    } else if (BACK_LABEL.equalsIgnoreCase(label)) {
                        Location location = context.getLocation(element);
                        if (context.isEnabled(BACK_BUTTON)) {
                            context.report(BACK_BUTTON, element, location,
                                "Back buttons are not standard on Android; see design guide's " +
                                "navigation section");
                        }
                    }
                }
            } else if (text.equals(CANCEL_LABEL) || text.equals(ANDROID_CANCEL_RESOURCE)) {
                if (isWrongCancelPosition(element)) {
                    reportCancelPosition(context, element);
                }
            } else if (text.equals(OK_LABEL) || text.equals(ANDROID_OK_RESOURCE)) {
                if (isWrongOkPosition(element)) {
                    reportOkPosition(context, element);
                }
            } else {
                boolean isYes = text.equals(ANDROID_YES_RESOURCE);
                if (isYes || text.equals(ANDROID_NO_RESOURCE)) {
                    Attr attribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_TEXT);
                    Location location = context.getLocation(attribute);
                    String message = String.format("%1$s actually returns \"%2$s\", not \"%3$s\"; "
                                    + "use %4$s instead or create a local string resource for %5$s",
                            text,
                            isYes ? OK_LABEL : CANCEL_LABEL,
                            isYes ? YES_LABEL : NO_LABEL,
                            isYes ? ANDROID_OK_RESOURCE : ANDROID_CANCEL_RESOURCE,
                            isYes ? YES_LABEL : NO_LABEL);
                    context.report(CASE, element, location, message);
                }
            }
        }
    }

    private static boolean parentDefinesSelectableItem(Element element) {
        String background = element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
        if (VALUE_SELECTABLE_ITEM_BACKGROUND.equals(background)) {
            return true;
        }

        Node parent = element.getParentNode();
        if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
            return parentDefinesSelectableItem((Element) parent);
        }

        return false;
    }

    /** Report the given OK button as being in the wrong position */
    private void reportOkPosition(XmlContext context, Element element) {
        report(context, element, false /*isCancel*/);
    }

    /** Report the given Cancel button as being in the wrong position */
    private void reportCancelPosition(XmlContext context, Element element) {
        report(context, element, true /*isCancel*/);
    }

    /**
     * We've found a resource reference to some label we're interested in ("OK",
     * "Cancel", "Back", ...). Record the corresponding name such that in the
     * next pass through the layouts we can check the context (for OK/Cancel the
     * button order etc).
     */
    private void foundResource(XmlContext context, String name, Element element) {
        if (!LintUtils.isEnglishResource(context, true)) {
            return;
        }

        if (!context.getProject().getReportIssues()) {
            // If this is a library project not being analyzed, ignore it
            return;
        }

        if (mApplicableResources == null) {
            mApplicableResources = new HashSet<String>();
        }

        mApplicableResources.add(STRING_PREFIX + name);

        // ALSO record all the other string resources in this file to pick up other
        // labels. If you define "OK" in one resource file and "Cancel" in another
        // this won't work, but that's probably not common and has lower overhead.
        Node parentNode = element.getParentNode();

        List<Element> items = LintUtils.getChildren(parentNode);
        if (mKeyToLabel == null) {
            mKeyToLabel = new HashMap<String, String>(items.size());
        }
        for (Element item : items) {
            String itemName = item.getAttribute(ATTR_NAME);
            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) {
                    String text = stripLabel(child.getNodeValue());
                    if (!text.isEmpty()) {
                        mKeyToLabel.put(itemName, text);
                        break;
                    }
                }
            }
        }
    }

    /** Report the given OK/Cancel button as being in the wrong position */
    private void report(XmlContext context, Element element, boolean isCancel) {
        if (!context.isEnabled(ORDER)) {
            return;
        }

        if (mIgnore != null && mIgnore.contains(element)) {
            return;
        }

        int target = context.getProject().getTargetSdk();
        if (target < 14) {
            // If you're only targeting pre-ICS UI's, this is not an issue
            return;
        }

        boolean mustCreateIcsLayout = false;
        if (context.getProject().getMinSdk() < 14) {
            // If you're *also* targeting pre-ICS UIs, then this reverse button
            // order is correct for layouts intended for pre-ICS and incorrect for
            // ICS layouts.
            //
            // Therefore, we need to know if this layout is an ICS layout or
            // a pre-ICS layout.
            boolean isIcsLayout = context.getFolderVersion() >= 14;
            if (!isIcsLayout) {
                // This layout is not an ICS layout. However, there *must* also be
                // an ICS layout here, or this button order will be wrong:
                File res = context.file.getParentFile().getParentFile();
                File[] resFolders = res.listFiles();
                String fileName = context.file.getName();
                if (resFolders != null) {
                    for (File folder : resFolders) {
                        String folderName = folder.getName();
                        if (folderName.startsWith(SdkConstants.FD_RES_LAYOUT)
                                && folderName.contains("-v14")) { //$NON-NLS-1$
                            File layout = new File(folder, fileName);
                            if (layout.exists()) {
                                // Yes, a v14 specific layout is available so this pre-ICS
                                // layout order is not a problem
                                return;
                            }
                        }
                    }
                }
                mustCreateIcsLayout = true;
            }
        }

        List<Element> buttons = LintUtils.getChildren(element.getParentNode());

        if (mIgnore == null) {
            mIgnore = new HashSet<Element>();
        }
        for (Element button : buttons) {
            // Mark all the siblings in the ignore list to ensure that we don't
            // report *both* the Cancel and the OK button in "OK | Cancel"
            mIgnore.add(button);
        }

        String message;
        if (isCancel) {
            message = "Cancel button should be on the left";
        } else {
            message = "OK button should be on the right";
        }

        if (mustCreateIcsLayout) {
            message = String.format(
                    "Layout uses the wrong button order for API >= 14: Create a " +
                    "`layout-v14/%1$s` file with opposite order: %2$s",
                    context.file.getName(), message);
        }

        // Show existing button order? We can only do that for LinearLayouts
        // since in for example a RelativeLayout the order of the elements may
        // not be the same as the visual order
        String layout = element.getParentNode().getNodeName();
        if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
            List<String> labelList = getLabelList(buttons);
            String wrong = describeButtons(labelList);
            sortButtons(labelList);
            String right = describeButtons(labelList);
            message += String.format(" (was \"%1$s\", should be \"%2$s\")", wrong, right);
        }

        Location location = context.getLocation(element);
        context.report(ORDER, element, location, message);
    }

    /**
     * Sort a list of label buttons into the expected order (Cancel on the left,
     * OK on the right
     */
    private static void sortButtons(List<String> labelList) {
        for (int i = 0, n = labelList.size(); i < n; i++) {
            String label = labelList.get(i);
            if (label.equalsIgnoreCase(CANCEL_LABEL) && i > 0) {
                swap(labelList, 0, i);
            } else if (label.equalsIgnoreCase(OK_LABEL) && i < n - 1) {
                swap(labelList, n - 1, i);
            }
        }
    }

    /** Swaps the strings at positions i and j */
    private static void swap(List<String> strings, int i, int j) {
        if (i != j) {
            String temp = strings.get(i);
            strings.set(i, strings.get(j));
            strings.set(j, temp);
        }
    }

    /** Creates a display string for a list of button labels, such as "Cancel | OK" */
    private static String describeButtons(List<String> labelList) {
        StringBuilder sb = new StringBuilder(80);
        for (String label : labelList) {
            if (sb.length() > 0) {
                sb.append(" | "); //$NON-NLS-1$
            }
            sb.append(label);
        }

        return sb.toString();
    }

    /** Returns the ordered list of button labels */
    private List<String> getLabelList(List<Element> views) {
        List<String> labels = new ArrayList<String>();

        if (mIgnore == null) {
            mIgnore = new HashSet<Element>();
        }

        for (Element view : views) {
            if (view.getTagName().equals(BUTTON)) {
                String text = view.getAttributeNS(ANDROID_URI, ATTR_TEXT);
                String label = getLabel(text);
                labels.add(label);

                // Mark all the siblings in the ignore list to ensure that we don't
                // report *both* the Cancel and the OK button in "OK | Cancel"
                mIgnore.add(view);
            }
        }

        return labels;
    }

    private String getLabel(String key) {
        String label = null;
        if (key.startsWith(ANDROID_STRING_PREFIX)) {
            if (key.equals(ANDROID_OK_RESOURCE)) {
                label = OK_LABEL;
            } else if (key.equals(ANDROID_CANCEL_RESOURCE)) {
                label = CANCEL_LABEL;
            }
        } else if (mKeyToLabel != null) {
            if (key.startsWith(STRING_PREFIX)) {
                label = mKeyToLabel.get(key.substring(STRING_PREFIX.length()));
            }
        }

        if (label == null) {
            label = key;
        }

        if (label.indexOf(' ') != -1 && label.indexOf('"') == -1) {
            label = '"' + label + '"';
        }

        return label;
    }

    /** Is the cancel button in the wrong position? It has to be on the left. */
    private static boolean isWrongCancelPosition(Element element) {
        return isWrongPosition(element, true /*isCancel*/);
    }

    /** Is the OK button in the wrong position? It has to be on the right. */
    private static boolean isWrongOkPosition(Element element) {
        return isWrongPosition(element, false /*isCancel*/);
    }

    private static boolean isInButtonBar(Element element) {
        assert element.getTagName().equals(BUTTON) : element.getTagName();
        Node parentNode = element.getParentNode();
        if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
            return false;
        }
        Element parent = (Element) parentNode;

        String style = parent.getAttribute(ATTR_STYLE);
        if (style != null && style.contains("buttonBarStyle")) { //$NON-NLS-1$
            return true;
        }

        // Don't warn about single Cancel / OK buttons
        if (LintUtils.getChildCount(parent) < 2) {
            return false;
        }

        String layout = parent.getTagName();
        if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
            String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
            if (VALUE_VERTICAL.equals(orientation)) {
                return false;
            }
        } else {
            return false;
        }

        // Ensure that all the children are buttons
        Node n = parent.getFirstChild();
        while (n != null) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                if (!BUTTON.equals(n.getNodeName())) {
                    return false;
                }
            }
            n = n.getNextSibling();
        }

        return true;
    }

    /** Is the given button in the wrong position? */
    private static boolean isWrongPosition(Element element, boolean isCancel) {
        Node parentNode = element.getParentNode();
        if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
            return false;
        }
        Element parent = (Element) parentNode;

        // Don't warn about single Cancel / OK buttons
        if (LintUtils.getChildCount(parent) < 2) {
            return false;
        }

        String layout = parent.getTagName();
        if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
            String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
            if (VALUE_VERTICAL.equals(orientation)) {
                return false;
            }

            if (isCancel) {
                Node n = element.getPreviousSibling();
                while (n != null) {
                    if (n.getNodeType() == Node.ELEMENT_NODE) {
                        return true;
                    }
                    n = n.getPreviousSibling();
                }
            } else {
                Node n = element.getNextSibling();
                while (n != null) {
                    if (n.getNodeType() == Node.ELEMENT_NODE) {
                        return true;
                    }
                    n = n.getNextSibling();
                }
            }

            return false;
        } else if (layout.equals(RELATIVE_LAYOUT) || layout.equals(PERCENT_RELATIVE_LAYOUT)) {
            // In RelativeLayouts, look for attachments which look like a clear sign
            // that the OK or Cancel buttons are out of order:
            //   -- a left attachment on a Cancel button (where the left attachment
            //      is a button; we don't want to complain if it's pointing to a spacer
            //      or image or progress indicator etc)
            //   -- a right-side parent attachment on a Cancel button (unless it's also
            //      attached on the left, e.g. a cancel button stretching across the
            //      layout)
            // etc.
            if (isCancel) {
                if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF)
                        && isButtonId(parent, element.getAttributeNS(ANDROID_URI,
                                ATTR_LAYOUT_TO_RIGHT_OF))) {
                    return true;
                }
                if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT) &&
                        !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
                    return true;
                }
            } else {
                if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF)
                        && isButtonId(parent, element.getAttributeNS(ANDROID_URI,
                                ATTR_LAYOUT_TO_RIGHT_OF))) {
                    return true;
                }
                if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT) &&
                        !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
                    return true;
                }
            }

            return false;
        } else {
            // TODO: Consider other button layouts - GridLayouts, custom views extending
            // LinearLayout etc?
            return false;
        }
    }

    /**
     * Returns true if the given attribute (in the Android namespace) is set to
     * true on the given element
     */
    private static boolean isTrue(Element element, String attribute) {
        return VALUE_TRUE.equals(element.getAttributeNS(ANDROID_URI, attribute));
    }

    /** Is the given target id the id of a {@code <Button>} within this RelativeLayout? */
    private static boolean isButtonId(Element parent, String targetId) {
        for (Element child : LintUtils.getChildren(parent)) {
            String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
            if (LintUtils.idReferencesMatch(id, targetId)) {
                return child.getTagName().equals(BUTTON);
            }
        }
        return false;
    }
}
