/*
 * 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.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_END;
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_ALIGN_PARENT_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_END_OF;
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_LAYOUT_TO_START_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.ATTR_VISIBILITY;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.tools.lint.checks.RequiredAttributeDetector.PERCENT_RELATIVE_LAYOUT;

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.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
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.Maps;

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.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
 * Check for potential item overlaps in a RelativeLayout when left- and
 * right-aligned text items are used.
 */
public class RelativeOverlapDetector extends LayoutDetector {
    public static final Issue ISSUE = Issue.create(
            "RelativeOverlap",
            "Overlapping items in RelativeLayout",
            "If relative layout has text or button items aligned to left and right " +
            "sides they can overlap each other due to localized text expansion " +
            "unless they have mutual constraints like `toEndOf`/`toStartOf`.",
            Category.I18N, 3, Severity.WARNING,
            new Implementation(RelativeOverlapDetector.class, Scope.RESOURCE_FILE_SCOPE));

    private static class LayoutNode {
        private enum Bucket {
            TOP, BOTTOM, SKIP
        }

        private final int mIndex;
        private boolean mProcessed;
        private final Element mNode;
        private Bucket mBucket;
        private LayoutNode mToLeft;
        private LayoutNode mToRight;
        private boolean mLastLeft;
        private boolean mLastRight;

        public LayoutNode(@NonNull Element node, int index) {
            mNode = node;
            mIndex = index;
            mProcessed = false;
            mLastLeft = true;
            mLastRight = true;
        }

        @NonNull
        public String getNodeId() {
            String nodeid = mNode.getAttributeNS(ANDROID_URI, ATTR_ID);
            if (nodeid.isEmpty()) {
                return String.format("%1$s-%2$d", mNode.getTagName(), mIndex);
            } else {
                return uniformId(nodeid);
            }
        }

        @NonNull
        public String getNodeTextId() {
            String text = mNode.getAttributeNS(ANDROID_URI, ATTR_TEXT);
            if (text.isEmpty()) {
                return getNodeId();
            } else {
                return uniformId(text);
            }
        }

        @NonNull
        @Override
        public String toString() {
            return getNodeTextId();
        }

        public boolean isInvisible() {
            String visibility = mNode.getAttributeNS(ANDROID_URI,
                    ATTR_VISIBILITY);
            return visibility.equals("gone") || visibility.equals("invisible");
        }

        /**
         * Determine if not can grow due to localization or not.
         */
        public boolean fixedWidth() {
            String width = mNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
            if (width.equals(VALUE_WRAP_CONTENT)) {
                // First check child nodes. If at least one of them is not
                // fixed-width,
                // treat whole layout as non-fixed-width
                NodeList childNodes = mNode.getChildNodes();
                for (int i = 0; i < childNodes.getLength(); i++) {
                    Node child = childNodes.item(i);
                    if (child.getNodeType() == Node.ELEMENT_NODE) {
                        LayoutNode childLayout = new LayoutNode((Element) child,
                                i);
                        if (!childLayout.fixedWidth()) {
                            return false;
                        }
                    }
                }
                // If node contains text attribute, consider it fixed-width if
                // text is hard-coded, otherwise it is not fixed-width.
                String text = mNode.getAttributeNS(ANDROID_URI, ATTR_TEXT);
                if (!text.isEmpty()) {
                    return !text.startsWith(PREFIX_RESOURCE_REF)
                        && !text.startsWith(PREFIX_THEME_REF);
                }

                String nodeName = mNode.getTagName();
                if (nodeName.contains("Image") || nodeName.contains("Progress")
                        || nodeName.contains("Radio")) {
                    return true;
                } else if (nodeName.contains("Button")
                        || nodeName.contains("Text")) {
                    return false;
                }
            }
            return true;
        }

        @NonNull
        public Element getNode() {
            return mNode;
        }

        /**
         * Process a node of a layout. Put it into one of three processing
         * units and determine its right and left neighbours.
         */
        public void processNode(@NonNull Map<String, LayoutNode> nodes) {
            if (mProcessed) {
                return;
            }
            mProcessed = true;

            if (isInvisible() ||
                hasAttr(ATTR_LAYOUT_ALIGN_RIGHT) ||
                hasAttr(ATTR_LAYOUT_ALIGN_END) ||
                hasAttr(ATTR_LAYOUT_ALIGN_LEFT) ||
                hasAttr(ATTR_LAYOUT_ALIGN_START)) {
                mBucket = Bucket.SKIP;
            } else if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
                mBucket = Bucket.TOP;
            } else if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
                mBucket = Bucket.BOTTOM;
            } else {
                if (hasAttr(ATTR_LAYOUT_ABOVE) || hasAttr(ATTR_LAYOUT_BELOW)) {
                    mBucket = Bucket.SKIP;
                } else {
                    String[] checkAlignment = { ATTR_LAYOUT_ALIGN_TOP,
                            ATTR_LAYOUT_ALIGN_BOTTOM,
                            ATTR_LAYOUT_ALIGN_BASELINE };
                    for (String alignment : checkAlignment) {
                        String value = mNode.getAttributeNS(ANDROID_URI,
                                alignment);
                        if (!value.isEmpty()) {
                            LayoutNode otherNode = nodes.get(uniformId(value));
                            if (otherNode != null) {
                                otherNode.processNode(nodes);
                                mBucket = otherNode.mBucket;
                            }
                        }
                    }
                }
            }
            if (mBucket == null) {
                mBucket = Bucket.TOP;
            }

            // Check relative placement
            boolean positioned = false;
            mToLeft = findNodeByAttr(nodes, ATTR_LAYOUT_TO_START_OF);
            if (mToLeft == null) {
                mToLeft = findNodeByAttr(nodes, ATTR_LAYOUT_TO_LEFT_OF);
            }
            // Avoid circular dependency
            for (LayoutNode n = mToLeft; n != null; n = n.mToLeft) {
              if (n.equals(this)) {
                mToLeft = null;
                mBucket = Bucket.SKIP;
                break;
              }
            }
            if (mToLeft != null) {
                mToLeft.mLastLeft = false;
                mLastRight = false;
                positioned = true;
            }
            mToRight = findNodeByAttr(nodes, ATTR_LAYOUT_TO_END_OF);
            if (mToRight == null) {
                mToRight = findNodeByAttr(nodes, ATTR_LAYOUT_TO_RIGHT_OF);
            }
            // Avoid circular dependency
            for (LayoutNode n = mToRight; n != null; n = n.mToRight) {
              if (n.equals(this)) {
                mToRight = null;
                mBucket = Bucket.SKIP;
                break;
              }
            }
            if (mToRight != null) {
                mToRight.mLastRight = false;
                mLastLeft = false;
                positioned = true;
            }

            if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_END)
                    || hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
                mLastRight = false;
                positioned = true;
            }
            if (hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_START)
                    || hasTrueAttr(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
                mLastLeft = false;
                positioned = true;
            }
            // Treat any node that does not have explicit relative placement
            // same as if it has layout_alignParentStart = true;
            if (!positioned) {
                mLastLeft = false;
            }
        }

        @NonNull
        public Set<LayoutNode> canGrowLeft() {
            Set<LayoutNode> nodes;
            if (mToRight != null) {
                nodes = mToRight.canGrowLeft();
            } else {
                nodes = new LinkedHashSet<LayoutNode>();
            }
            if (!fixedWidth()) {
                nodes.add(this);
            }
            return nodes;
        }

        @NonNull
        public Set<LayoutNode> canGrowRight() {
            Set<LayoutNode> nodes;
            if (mToLeft != null) {
                nodes = mToLeft.canGrowRight();
            } else {
                nodes = new LinkedHashSet<LayoutNode>();
            }
            if (!fixedWidth()) {
                nodes.add(this);
            }
            return nodes;
        }

        /**
         * Determines if not should be skipped from checking.
         */
        public boolean skip() {
            if (mBucket == Bucket.SKIP) {
                return true;
            }

            // Skip all includes and Views
            return mNode.getTagName().equals(VIEW_INCLUDE)
                    || mNode.getTagName().equals(VIEW);
        }

        public boolean sameBucket(@NonNull LayoutNode node) {
            return mBucket == node.mBucket;
        }

        @Nullable
        private LayoutNode findNodeByAttr(
                @NonNull Map<String, LayoutNode> nodes,
                @NonNull String attrName) {
            String value = mNode.getAttributeNS(ANDROID_URI, attrName);
            if (!value.isEmpty()) {
                return nodes.get(uniformId(value));
            } else {
                return null;
            }
        }

        private boolean hasAttr(@NonNull String key) {
            return mNode.hasAttributeNS(ANDROID_URI, key);
        }

        private boolean hasTrueAttr(@NonNull String key) {
            return mNode.getAttributeNS(ANDROID_URI, key).equals(VALUE_TRUE);
        }

        @NonNull
        private static String uniformId(@NonNull String value) {
            return value.replaceFirst("@\\+", "@");
        }
    }

    public RelativeOverlapDetector() {
    }

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

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        // Traverse all child elements
        NodeList childNodes = element.getChildNodes();
        int count = childNodes.getLength();
        Map<String, LayoutNode> nodes = Maps.newHashMap();
        for (int i = 0; i < count; i++) {
            Node node = childNodes.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                LayoutNode ln = new LayoutNode((Element) node, i);
                nodes.put(ln.getNodeId(), ln);
            }
        }

        // Node map is populated, recalculate nodes sizes
        for (LayoutNode ln : nodes.values()) {
            ln.processNode(nodes);
        }
        for (LayoutNode right : nodes.values()) {
            if (!right.mLastLeft || right.skip()) {
                continue;
            }
            Set<LayoutNode> canGrowLeft = right.canGrowLeft();
            for (LayoutNode left : nodes.values()) {
                if (left == right || !left.mLastRight || left.skip()
                        || !left.sameBucket(right)) {
                    continue;
                }
                Set<LayoutNode> canGrowRight = left.canGrowRight();
                if (!canGrowLeft.isEmpty() || !canGrowRight.isEmpty()) {
                    canGrowRight.addAll(canGrowLeft);
                    LayoutNode nodeToBlame = right;
                    LayoutNode otherNode = left;
                    if (!canGrowRight.contains(right)
                            && canGrowRight.contains(left)) {
                        nodeToBlame = left;
                        otherNode = right;
                    }
                    context.report(ISSUE, nodeToBlame.getNode(),
                            context.getLocation(nodeToBlame.getNode()),
                            String.format(
                                    "`%1$s` can overlap `%2$s` if %3$s %4$s due to localized text expansion",
                                    nodeToBlame.getNodeId(), otherNode.getNodeId(),
                                    Joiner.on(", ").join(canGrowRight),
                                    canGrowRight.size() > 1 ? "grow" : "grows"));
                }
            }
        }
    }
}
