/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.ide.eclipse.adt.internal.editors.layout.refactoring;

import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
import static com.android.ide.common.layout.LayoutConstants.ATTR_BACKGROUND;
import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WEIGHT;
import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_BOTTOM;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_HORIZONTAL;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_CENTER_VERTICAL;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_HORIZONTAL;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_FILL_VERTICAL;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_LEFT;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_RIGHT;
import static com.android.ide.common.layout.LayoutConstants.GRAVITY_VALUE_TOP;
import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT;
import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
import static com.android.ide.common.layout.LayoutConstants.RELATIVE_LAYOUT;
import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE;
import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP;
import static com.android.ide.common.layout.LayoutConstants.VALUE_TRUE;
import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.util.Pair;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.text.edits.MultiTextEdit;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Helper class which performs the bulk of the layout conversion to relative layout
 * <p>
 * Future enhancements:
 * <ul>
 * <li>Render the layout at multiple screen sizes and analyze how the widgets move and
 * stretch and use that to add in additional constraints
 * <li> Adapt the LinearLayout analysis code to work with TableLayouts and TableRows as well
 * (just need to tweak the "isVertical" interpretation to account for the different defaults,
 * and perhaps do something about column size properties.
 * <li> We need to take into account existing margins and clear/update them
 * </ul>
 */
class RelativeLayoutConversionHelper {
    private final MultiTextEdit mRootEdit;
    private final boolean mFlatten;
    private final Element mLayout;
    private final ChangeLayoutRefactoring mRefactoring;
    private final CanvasViewInfo mRootView;
    private List<Element> mDeletedElements;

    RelativeLayoutConversionHelper(ChangeLayoutRefactoring refactoring,
            Element layout, boolean flatten, MultiTextEdit rootEdit, CanvasViewInfo rootView) {
        mRefactoring = refactoring;
        mLayout = layout;
        mFlatten = flatten;
        mRootEdit = rootEdit;
        mRootView = rootView;
    }

    /** Performs conversion from any layout to a RelativeLayout */
    public void convertToRelative() {
        if (mRootView == null) {
            return;
        }

        // Locate the view for the layout
        CanvasViewInfo layoutView = findViewForElement(mRootView, mLayout);
        if (layoutView == null || layoutView.getChildren().size() == 0) {
            // No children. THAT was an easy conversion!
            return;
        }

        // Study the layout and get information about how to place individual elements
        List<View> views = analyzeLayout(layoutView);

        // Create/update relative layout constraints
        createAttachments(views);
    }

    /** Returns the elements that were deleted, or null */
    List<Element> getDeletedElements() {
        return mDeletedElements;
    }

    /**
     * Analyzes the given view hierarchy and produces a list of {@link View} objects which
     * contain placement information for each element
     */
    private List<View> analyzeLayout(CanvasViewInfo layoutView) {
        EdgeList edgeList = new EdgeList(layoutView);
        mDeletedElements = edgeList.getDeletedElements();
        deleteRemovedElements(mDeletedElements);

        List<Integer> columnOffsets = edgeList.getColumnOffsets();
        List<Integer> rowOffsets = edgeList.getRowOffsets();

        // Compute x/y offsets for each row/column index
        int[] left = new int[columnOffsets.size()];
        int[] top = new int[rowOffsets.size()];

        Map<Integer, Integer> xToCol = new HashMap<Integer, Integer>();
        int columnIndex = 0;
        for (Integer offset : columnOffsets) {
            left[columnIndex] = offset;
            xToCol.put(offset, columnIndex++);
        }
        Map<Integer, Integer> yToRow = new HashMap<Integer, Integer>();
        int rowIndex = 0;
        for (Integer offset : rowOffsets) {
            top[rowIndex] = offset;
            yToRow.put(offset, rowIndex++);
        }

        // Create a complete list of view objects
        List<View> views = createViews(edgeList, columnOffsets);
        initializeSpans(edgeList, columnOffsets, rowOffsets, xToCol, yToRow);

        // Sanity check
        for (View view : views) {
            assert view.getLeftEdge() == left[view.mCol];
            assert view.getTopEdge() == top[view.mRow];
            assert view.getRightEdge() == left[view.mCol+view.mColSpan];
            assert view.getBottomEdge() == top[view.mRow+view.mRowSpan];
        }

        // Ensure that every view has a proper id such that it can be referred to
        // with a constraint
        initializeIds(edgeList, views);

        // Attempt to lay the views out in a grid with constraints (though not that widgets
        // can overlap as well)
        Grid grid = new Grid(views, left, top);
        computeKnownConstraints(views, edgeList);
        computeHorizontalConstraints(grid);
        computeVerticalConstraints(grid);

        return views;
    }

    /** Produces a list of {@link View} objects from an {@link EdgeList} */
    private List<View> createViews(EdgeList edgeList, List<Integer> columnOffsets) {
        List<View> views = new ArrayList<View>();
        for (Integer offset : columnOffsets) {
            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
            if (leftEdgeViews == null) {
                // must have been a right edge
                continue;
            }
            for (View view : leftEdgeViews) {
                views.add(view);
            }
        }
        return views;
    }

    /** Removes any elements targeted for deletion */
    private void deleteRemovedElements(List<Element> delete) {
        if (mFlatten && delete.size() > 0) {
            for (Element element : delete) {
                mRefactoring.removeElementTags(mRootEdit, element, delete,
                        !AdtPrefs.getPrefs().getFormatGuiXml() /*changeIndentation*/);
            }
        }
    }

    /** Ensures that every element has an id such that it can be referenced from a constraint */
    private void initializeIds(EdgeList edgeList, List<View> views) {
        // Ensure that all views have a valid id
        for (View view : views) {
            String id = mRefactoring.ensureHasId(mRootEdit, view.mElement, null);
            edgeList.setIdAttributeValue(view, id);
        }
    }

    /**
     * Initializes the column and row indices, as well as any column span and row span
     * values
     */
    private void initializeSpans(EdgeList edgeList, List<Integer> columnOffsets,
            List<Integer> rowOffsets, Map<Integer, Integer> xToCol, Map<Integer, Integer> yToRow) {
        // Now initialize table view row, column and spans
        for (Integer offset : columnOffsets) {
            List<View> leftEdgeViews = edgeList.getLeftEdgeViews(offset);
            if (leftEdgeViews == null) {
                // must have been a right edge
                continue;
            }
            for (View view : leftEdgeViews) {
                Integer col = xToCol.get(view.getLeftEdge());
                assert col != null;
                Integer end = xToCol.get(view.getRightEdge());
                assert end != null;

                view.mCol = col;
                view.mColSpan = end - col;
            }
        }

        for (Integer offset : rowOffsets) {
            List<View> topEdgeViews = edgeList.getTopEdgeViews(offset);
            if (topEdgeViews == null) {
                // must have been a bottom edge
                continue;
            }
            for (View view : topEdgeViews) {
                Integer row = yToRow.get(view.getTopEdge());
                assert row != null;
                Integer end = yToRow.get(view.getBottomEdge());
                assert end != null;

                view.mRow = row;
                view.mRowSpan = end - row;
            }
        }
    }

    /**
     * Creates refactoring edits which adds or updates constraints for the given list of
     * views
     */
    private void createAttachments(List<View> views) {
        // Make the attachments
        String namespace = mRefactoring.getAndroidNamespacePrefix();
        for (View view : views) {
            for (Pair<String, String> constraint : view.getHorizConstraints()) {
                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
                        namespace, constraint.getFirst(), constraint.getSecond());
            }
            for (Pair<String, String> constraint : view.getVerticalConstraints()) {
                mRefactoring.setAttribute(mRootEdit, view.mElement, ANDROID_URI,
                        namespace, constraint.getFirst(), constraint.getSecond());
            }
        }
    }

    /**
     * Analyzes the existing layouts and layout parameter objects in the document to infer
     * constraints for layout types that we know about - such as LinearLayout baseline
     * alignment, weights, gravity, etc.
     */
    private void computeKnownConstraints(List<View> views, EdgeList edgeList) {
        // List of parent layout elements we've already processed. We iterate through all
        // the -children-, and we ask each for its element parent (which won't have a view)
        // and we look at the parent's layout attributes and its children layout constraints,
        // and then we stash away constraints that we can infer. This means that we will
        // encounter the same parent for every sibling, so that's why there's a map to
        // prevent duplicate work.
        Set<Node> seen = new HashSet<Node>();

        for (View view : views) {
            Element element = view.getElement();
            Node parent = element.getParentNode();
            if (seen.contains(parent)) {
                continue;
            }
            seen.add(parent);

            if (parent.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            Element layout = (Element) parent;
            String layoutName = layout.getTagName();

            if (LINEAR_LAYOUT.equals(layoutName)) {
                analyzeLinearLayout(edgeList, layout);
            } else if (RELATIVE_LAYOUT.equals(layoutName)) {
                analyzeRelativeLayout(edgeList, layout);
            } else {
                // Some other layout -- add more conditional handling here
                // for framelayout, tables, etc.
            }
        }
    }

    public static final int GRAVITY_LEFT = 1 << 0;
    public static final int GRAVITY_RIGHT = 1<< 1;
    public static final int GRAVITY_CENTER_HORIZ = 1 << 2;
    public static final int GRAVITY_FILL_HORIZ = 1 << 3;
    public static final int GRAVITY_CENTER_VERT = 1 << 4;
    public static final int GRAVITY_FILL_VERT = 1 << 5;
    public static final int GRAVITY_TOP = 1 << 6;
    public static final int GRAVITY_BOTTOM = 1 << 7;
    public static final int GRAVITY_HORIZ_MASK = GRAVITY_CENTER_HORIZ | GRAVITY_FILL_HORIZ
            | GRAVITY_LEFT | GRAVITY_RIGHT;
    public static final int GRAVITY_VERT_MASK = GRAVITY_CENTER_VERT | GRAVITY_FILL_VERT
            | GRAVITY_TOP | GRAVITY_BOTTOM;

    /**
     * Returns the gravity of the given element
     *
     * @param element the element to look up the gravity for
     * @return a bit mask corresponding to the selected gravities
     */
    public static int getGravity(Element element) {
        String gravityString = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY);
        return getGravity(gravityString, GRAVITY_LEFT | GRAVITY_TOP);
    }

    /**
     * Returns the gravity bitmask for the given gravity string description
     *
     * @param gravityString the gravity string description
     * @param defaultMask the default/initial bitmask to start with
     * @return a bitmask corresponding to the gravity description
     */
    public static int getGravity(String gravityString, int defaultMask) {
        int gravity = defaultMask;
        if (gravityString != null && gravityString.length() > 0) {
            String[] anchors = gravityString.split("\\|"); //$NON-NLS-1$
            for (String anchor : anchors) {
                if (GRAVITY_VALUE_CENTER.equals(anchor)) {
                    gravity = GRAVITY_CENTER_HORIZ | GRAVITY_CENTER_VERT;
                } else if (GRAVITY_VALUE_FILL.equals(anchor)) {
                        gravity = GRAVITY_FILL_HORIZ | GRAVITY_FILL_VERT;
                } else if (GRAVITY_VALUE_CENTER_VERTICAL.equals(anchor)) {
                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_CENTER_VERT;
                } else if (GRAVITY_VALUE_CENTER_HORIZONTAL.equals(anchor)) {
                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_CENTER_HORIZ;
                } else if (GRAVITY_VALUE_FILL_VERTICAL.equals(anchor)) {
                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_FILL_VERT;
                } else if (GRAVITY_VALUE_FILL_HORIZONTAL.equals(anchor)) {
                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_FILL_HORIZ;
                } else if (GRAVITY_VALUE_TOP.equals(anchor)) {
                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_TOP;
                } else if (GRAVITY_VALUE_BOTTOM.equals(anchor)) {
                    gravity = (gravity & GRAVITY_HORIZ_MASK) | GRAVITY_BOTTOM;
                } else if (GRAVITY_VALUE_LEFT.equals(anchor)) {
                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_LEFT;
                } else if (GRAVITY_VALUE_RIGHT.equals(anchor)) {
                    gravity = (gravity & GRAVITY_VERT_MASK) | GRAVITY_RIGHT;
                } else {
                    // "clip" not supported
                }
            }
        }

        return gravity;
    }

    /**
     * Returns the layout weight of of the given child of a LinearLayout, or 0.0 if it
     * does not define a weight
     */
    private float getWeight(Element linearLayoutChild) {
        String weight = linearLayoutChild.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);
        if (weight != null && weight.length() > 0) {
            try {
                return Float.parseFloat(weight);
            } catch (NumberFormatException nfe) {
                AdtPlugin.log(nfe, "Invalid weight %1$s", weight);
            }
        }

        return 0.0f;
    }

    /**
     * Returns the sum of all the layout weights of the children in the given LinearLayout
     *
     * @param linearLayout the layout to compute the total sum for
     * @return the total sum of all the layout weights in the given layout
     */
    private float getWeightSum(Element linearLayout) {
        float sum = 0;
        for (Element child : DomUtilities.getChildren(linearLayout)) {
            sum += getWeight(child);
        }

        return sum;
    }

    /**
     * Analyzes the given LinearLayout and updates the constraints to reflect
     * relationships it can infer - based on baseline alignment, gravity, order and
     * weights. This method also removes "0dip" as a special width/height used in
     * LinearLayouts with weight distribution.
     */
    private void analyzeLinearLayout(EdgeList edgeList, Element layout) {
        boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
                ATTR_ORIENTATION));
        View baselineRef = null;
        if (!isVertical &&
            !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED))) {
            // Baseline alignment. Find the tallest child and set it as the baseline reference.
            int tallestHeight = 0;
            View tallest = null;
            for (Element child : DomUtilities.getChildren(layout)) {
                View view = edgeList.getView(child);
                if (view != null && view.getHeight() > tallestHeight) {
                    tallestHeight = view.getHeight();
                    tallest = view;
                }
            }
            if (tallest != null) {
                baselineRef = tallest;
            }
        }

        float weightSum = getWeightSum(layout);
        float cumulativeWeight = 0;

        List<Element> children = DomUtilities.getChildren(layout);
        String prevId = null;
        boolean isFirstChild = true;
        boolean linkBackwards = true;
        boolean linkForwards = false;

        for (int index = 0, childCount = children.size(); index < childCount; index++) {
            Element child = children.get(index);

            View childView = edgeList.getView(child);
            if (childView == null) {
                // Could be a nested layout that is being removed etc
                prevId = null;
                isFirstChild = false;
                continue;
            }

            // Look at the layout_weight attributes and determine whether we should be
            // attached on the bottom/right or on the top/left
            if (weightSum > 0.0f) {
                float weight = getWeight(child);

                // We can't emulate a LinearLayout where multiple children have positive
                // weights. However, we CAN support the common scenario where a single
                // child has a non-zero weight, and all children after it are pushed
                // to the end and the weighted child fills the remaining space.
                if (cumulativeWeight == 0 && weight > 0) {
                    // See if we have a bottom/right edge to attach the forwards link to
                    // (at the end of the forwards chains). Only if so can we link forwards.
                    View referenced;
                    if (isVertical) {
                        referenced = edgeList.getSharedBottomEdge(layout);
                    } else {
                        referenced = edgeList.getSharedRightEdge(layout);
                    }
                    if (referenced != null) {
                        linkForwards = true;
                    }
                } else if (cumulativeWeight > 0) {
                    linkBackwards = false;
                }

                cumulativeWeight += weight;
            }

            analyzeGravity(edgeList, layout, isVertical, child, childView);
            convert0dipToWrapContent(child);

            // Chain elements together in the flow direction of the linear layout
            if (prevId != null) { // No constraint for first child
                if (linkBackwards) {
                    if (isVertical) {
                        childView.addVerticalConstraint(ATTR_LAYOUT_BELOW, prevId);
                    } else {
                        childView.addHorizConstraint(ATTR_LAYOUT_TO_RIGHT_OF, prevId);
                    }
                }
            } else if (isFirstChild) {
                assert linkBackwards;

                // First element; attach it to the parent if we can
                if (isVertical) {
                    View referenced = edgeList.getSharedTopEdge(layout);
                    if (referenced != null) {
                        if (isAncestor(referenced.getElement(), child)) {
                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
                                VALUE_TRUE);
                        } else {
                            childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
                                    referenced.getId());
                        }
                    }
                } else {
                    View referenced = edgeList.getSharedLeftEdge(layout);
                    if (referenced != null) {
                        if (isAncestor(referenced.getElement(), child)) {
                            childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
                                    VALUE_TRUE);
                        } else {
                            childView.addHorizConstraint(
                                    ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
                        }
                    }
                }
            }

            if (linkForwards) {
                if (index < (childCount - 1)) {
                    Element nextChild = children.get(index + 1);
                    String nextId = mRefactoring.ensureHasId(mRootEdit, nextChild, null);
                    if (nextId != null) {
                        if (isVertical) {
                            childView.addVerticalConstraint(ATTR_LAYOUT_ABOVE, nextId);
                        } else {
                            childView.addHorizConstraint(ATTR_LAYOUT_TO_LEFT_OF, nextId);
                        }
                    }
                } else {
                    // Attach to right/bottom edge of the layout
                    if (isVertical) {
                        View referenced = edgeList.getSharedBottomEdge(layout);
                        if (referenced != null) {
                            if (isAncestor(referenced.getElement(), child)) {
                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
                                    VALUE_TRUE);
                            } else {
                                childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
                                        referenced.getId());
                            }
                        }
                    } else {
                        View referenced = edgeList.getSharedRightEdge(layout);
                        if (referenced != null) {
                            if (isAncestor(referenced.getElement(), child)) {
                                childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
                                        VALUE_TRUE);
                            } else {
                                childView.addHorizConstraint(
                                        ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
                            }
                        }
                    }
                }
            }

            if (baselineRef != null && baselineRef.getId() != null
                    && !baselineRef.getId().equals(childView.getId())) {
                assert !isVertical;
                // Only align if they share the same gravity
                if ((childView.getGravity() & GRAVITY_VERT_MASK) ==
                        (baselineRef.getGravity() & GRAVITY_VERT_MASK)) {
                    childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_BASELINE, baselineRef.getId());
                }
            }

            prevId = mRefactoring.ensureHasId(mRootEdit, child, null);
            isFirstChild = false;
        }
    }

    /**
     * Checks the layout "gravity" value for the given child and updates the constraints
     * to account for the gravity
     */
    private int analyzeGravity(EdgeList edgeList, Element layout, boolean isVertical,
            Element child, View childView) {
        // Use gravity to constrain elements in the axis orthogonal to the
        // direction of the layout
        int gravity = childView.getGravity();
        if (isVertical) {
            if ((gravity & GRAVITY_RIGHT) != 0) {
                View referenced = edgeList.getSharedRightEdge(layout);
                if (referenced != null) {
                    if (isAncestor(referenced.getElement(), child)) {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
                                VALUE_TRUE);
                    } else {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
                                referenced.getId());
                    }
                }
            } else if ((gravity & GRAVITY_CENTER_HORIZ) != 0) {
                View referenced1 = edgeList.getSharedLeftEdge(layout);
                View referenced2 = edgeList.getSharedRightEdge(layout);
                if (referenced1 != null && referenced2 == referenced1) {
                    if (isAncestor(referenced1.getElement(), child)) {
                        childView.addHorizConstraint(ATTR_LAYOUT_CENTER_HORIZONTAL,
                                VALUE_TRUE);
                    }
                }
            } else if ((gravity & GRAVITY_FILL_HORIZ) != 0) {
                View referenced1 = edgeList.getSharedLeftEdge(layout);
                View referenced2 = edgeList.getSharedRightEdge(layout);
                if (referenced1 != null && referenced2 == referenced1) {
                    if (isAncestor(referenced1.getElement(), child)) {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
                                VALUE_TRUE);
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
                                VALUE_TRUE);
                    } else {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
                                referenced1.getId());
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_RIGHT,
                                referenced2.getId());
                    }
                }
            } else if ((gravity & GRAVITY_LEFT) != 0) {
                View referenced = edgeList.getSharedLeftEdge(layout);
                if (referenced != null) {
                    if (isAncestor(referenced.getElement(), child)) {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_PARENT_LEFT,
                                VALUE_TRUE);
                    } else {
                        childView.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT,
                                referenced.getId());
                    }
                }
            }
        } else {
            // Handle horizontal layout: perform vertical gravity attachments
            if ((gravity & GRAVITY_BOTTOM) != 0) {
                View referenced = edgeList.getSharedBottomEdge(layout);
                if (referenced != null) {
                    if (isAncestor(referenced.getElement(), child)) {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
                                VALUE_TRUE);
                    } else {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
                                referenced.getId());
                    }
                }
            } else if ((gravity & GRAVITY_CENTER_VERT) != 0) {
                View referenced1 = edgeList.getSharedTopEdge(layout);
                View referenced2 = edgeList.getSharedBottomEdge(layout);
                if (referenced1 != null && referenced2 == referenced1) {
                    if (isAncestor(referenced1.getElement(), child)) {
                        childView.addVerticalConstraint(ATTR_LAYOUT_CENTER_VERTICAL,
                                VALUE_TRUE);
                    }
                }
            } else if ((gravity & GRAVITY_FILL_VERT) != 0) {
                View referenced1 = edgeList.getSharedTopEdge(layout);
                View referenced2 = edgeList.getSharedBottomEdge(layout);
                if (referenced1 != null && referenced2 == referenced1) {
                    if (isAncestor(referenced1.getElement(), child)) {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
                                VALUE_TRUE);
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
                                VALUE_TRUE);
                    } else {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
                                referenced1.getId());
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
                                referenced2.getId());
                    }
                }
            } else if ((gravity & GRAVITY_TOP) != 0) {
                View referenced = edgeList.getSharedTopEdge(layout);
                if (referenced != null) {
                    if (isAncestor(referenced.getElement(), child)) {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_PARENT_TOP,
                                VALUE_TRUE);
                    } else {
                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
                                referenced.getId());
                    }
                }
            }
        }
        return gravity;
    }

    /** Converts 0dip values in layout_width and layout_height to wrap_content instead */
    private void convert0dipToWrapContent(Element child) {
        // Must convert layout_height="0dip" to layout_height="wrap_content".
        // 0dip is a special trick used in linear layouts in the presence of
        // weights where 0dip ensures that the height of the view is not taken
        // into account when distributing the weights. However, when converted
        // to RelativeLayout this will instead cause the view to actually be assigned
        // 0 height.
        String height = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
        // 0dip, 0dp, 0px, etc
        if (height != null && height.startsWith("0")) { //$NON-NLS-1$
            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_HEIGHT,
                    VALUE_WRAP_CONTENT);
        }
        String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
        if (width != null && width.startsWith("0")) { //$NON-NLS-1$
            mRefactoring.setAttribute(mRootEdit, child, ANDROID_URI,
                    mRefactoring.getAndroidNamespacePrefix(), ATTR_LAYOUT_WIDTH,
                    VALUE_WRAP_CONTENT);
        }
    }

    /**
     * Analyzes an embedded RelativeLayout within a layout hierarchy and updates the
     * constraints in the EdgeList with those relationships which can continue in the
     * outer single RelativeLayout.
     */
    private void analyzeRelativeLayout(EdgeList edgeList, Element layout) {
        NodeList children = layout.getChildNodes();
        for (int i = 0, n = children.getLength(); i < n; i++) {
            Node node = children.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element child = (Element) node;
                View childView = edgeList.getView(child);
                if (childView == null) {
                    // Could be a nested layout that is being removed etc
                    continue;
                }

                NamedNodeMap attributes = child.getAttributes();
                for (int j = 0, m = attributes.getLength(); j < m; j++) {
                    Attr attribute = (Attr) attributes.item(j);
                    String name = attribute.getLocalName();
                    String value = attribute.getValue();
                    if (name.equals(ATTR_LAYOUT_WIDTH)
                            || name.equals(ATTR_LAYOUT_HEIGHT)) {
                        // Ignore these for now
                    } else if (name.startsWith(ATTR_LAYOUT_PREFIX)
                            && ANDROID_URI.equals(attribute.getNamespaceURI())) {
                        // Determine if the reference is to a known edge
                        String id = getIdBasename(value);
                        if (id != null) {
                            View referenced = edgeList.getView(id);
                            if (referenced != null) {
                                // This is a valid reference, so preserve
                                // the attribute
                                if (name.equals(ATTR_LAYOUT_BELOW) ||
                                        name.equals(ATTR_LAYOUT_ABOVE) ||
                                        name.equals(ATTR_LAYOUT_ALIGN_TOP) ||
                                        name.equals(ATTR_LAYOUT_ALIGN_BOTTOM) ||
                                        name.equals(ATTR_LAYOUT_ALIGN_BASELINE)) {
                                    // Vertical constraint
                                    childView.addVerticalConstraint(name, value);
                                } else if (name.equals(ATTR_LAYOUT_ALIGN_LEFT) ||
                                        name.equals(ATTR_LAYOUT_TO_LEFT_OF) ||
                                        name.equals(ATTR_LAYOUT_TO_RIGHT_OF) ||
                                        name.equals(ATTR_LAYOUT_ALIGN_RIGHT)) {
                                    // Horizontal constraint
                                    childView.addHorizConstraint(name, value);
                                } else {
                                    // We don't expect this
                                    assert false : name;
                                }
                            } else {
                                // Reference to some layout that is not included here.
                                // TODO: See if the given layout has an edge
                                // that corresponds to one of our known views
                                // so we can adjust the constraints and keep it after all.
                            }
                        } else {
                            // It's a parent-relative constraint (such
                            // as aligning with a parent edge, or centering
                            // in the parent view)
                            boolean remove = true;
                            if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
                                View referenced = edgeList.getSharedLeftEdge(layout);
                                if (referenced != null) {
                                    if (isAncestor(referenced.getElement(), child)) {
                                        childView.addHorizConstraint(name, VALUE_TRUE);
                                    } else {
                                        childView.addHorizConstraint(
                                                ATTR_LAYOUT_ALIGN_LEFT, referenced.getId());
                                    }
                                    remove = false;
                                }
                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
                                View referenced = edgeList.getSharedRightEdge(layout);
                                if (referenced != null) {
                                    if (isAncestor(referenced.getElement(), child)) {
                                        childView.addHorizConstraint(name, VALUE_TRUE);
                                    } else {
                                        childView.addHorizConstraint(
                                            ATTR_LAYOUT_ALIGN_RIGHT, referenced.getId());
                                    }
                                    remove = false;
                                }
                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_TOP)) {
                                View referenced = edgeList.getSharedTopEdge(layout);
                                if (referenced != null) {
                                    if (isAncestor(referenced.getElement(), child)) {
                                        childView.addVerticalConstraint(name, VALUE_TRUE);
                                    } else {
                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP,
                                                referenced.getId());
                                    }
                                    remove = false;
                                }
                            } else if (name.equals(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM)) {
                                View referenced = edgeList.getSharedBottomEdge(layout);
                                if (referenced != null) {
                                    if (isAncestor(referenced.getElement(), child)) {
                                        childView.addVerticalConstraint(name, VALUE_TRUE);
                                    } else {
                                        childView.addVerticalConstraint(ATTR_LAYOUT_ALIGN_BOTTOM,
                                                referenced.getId());
                                    }
                                    remove = false;
                                }
                            }

                            boolean alignWithParent =
                                    name.equals(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING);
                            if (remove && alignWithParent) {
                                // TODO - look for this one AFTER we have processed
                                // everything else, and then set constraints as necessary
                                // IF there are no other conflicting constraints!
                            }

                            // Otherwise it's some kind of centering which we don't support
                            // yet.

                            // TODO: Find a way to determine whether we have
                            // a corresponding edge for the parent (e.g. if
                            // the ViewInfo bounds match our outer parent or
                            // some other edge) and if so, substitute for that
                            // id.
                            // For example, if this element was centered
                            // horizontally in a RelativeLayout that actually
                            // occupies the entire width of our outer layout,
                            // then it can be preserved after all!

                            if (remove) {
                                if (name.startsWith("layout_margin")) { //$NON-NLS-1$
                                    continue;
                                }

                                // Remove unknown attributes?
                                // It's too early to do this, because we may later want
                                // to *set* this value and it would result in an overlapping edits
                                // exception. Therefore, we need to RECORD which attributes should
                                // be removed, which lines should have its indentation adjusted
                                // etc and finally process it all at the end!
                                //mRefactoring.removeAttribute(mRootEdit, child,
                                //        attribute.getNamespaceURI(), name);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Given {@code @id/foo} or {@code @+id/foo}, returns foo. Note that given foo it will
     * return null.
     */
    private static String getIdBasename(String id) {
        if (id.startsWith(NEW_ID_PREFIX)) {
            return id.substring(NEW_ID_PREFIX.length());
        } else if (id.startsWith(ID_PREFIX)) {
            return id.substring(ID_PREFIX.length());
        }

        return null;
    }

    /** Returns true if the given second argument is a descendant of the first argument */
    private static boolean isAncestor(Node ancestor, Node node) {
        while (node != null) {
            if (node == ancestor) {
                return true;
            }
            node = node.getParentNode();
        }
        return false;
    }

    /**
     * Computes horizontal constraints for the views in the grid for any remaining views
     * that do not have constraints (as the result of the analysis of known layouts). This
     * will look at the rendered layout coordinates and attempt to connect elements based
     * on a spatial layout in the grid.
     */
    private void computeHorizontalConstraints(Grid grid) {
        int columns = grid.getColumns();

        String attachLeftProperty = ATTR_LAYOUT_ALIGN_PARENT_LEFT;
        String attachLeftValue = VALUE_TRUE;
        int marginLeft = 0;
        for (int col = 0; col < columns; col++) {
            if (!grid.colContainsTopLeftCorner(col)) {
                // Just accumulate margins for the next column
                marginLeft += grid.getColumnWidth(col);
            } else {
                // Add horizontal attachments
                String firstId = null;
                for (View view : grid.viewsStartingInCol(col, true)) {
                    assert view.getId() != null;
                    if (firstId == null) {
                        firstId = view.getId();
                        if (view.isConstrainedHorizontally()) {
                            // Nothing to do -- we already have an accurate position for
                            // this view
                        } else if (attachLeftProperty != null) {
                            view.addHorizConstraint(attachLeftProperty, attachLeftValue);
                            if (marginLeft > 0) {
                                view.addHorizConstraint(ATTR_LAYOUT_MARGIN_LEFT,
                                        String.format(VALUE_N_DP, marginLeft));
                                marginLeft = 0;
                            }
                        } else {
                            assert false;
                        }
                    } else if (!view.isConstrainedHorizontally()) {
                        view.addHorizConstraint(ATTR_LAYOUT_ALIGN_LEFT, firstId);
                    }
                }
            }

            // Figure out edge for the next column
            View view = grid.findRightEdgeView(col);
            if (view != null) {
                assert view.getId() != null;
                attachLeftProperty = ATTR_LAYOUT_TO_RIGHT_OF;
                attachLeftValue = view.getId();

                marginLeft = 0;
            } else if (marginLeft == 0) {
                marginLeft = grid.getColumnWidth(col);
            }
        }
    }

    /**
     * Performs vertical layout just like the {@link #computeHorizontalConstraints} method
     * did horizontally
     */
    private void computeVerticalConstraints(Grid grid) {
        int rows = grid.getRows();

        String attachTopProperty = ATTR_LAYOUT_ALIGN_PARENT_TOP;
        String attachTopValue = VALUE_TRUE;
        int marginTop = 0;
        for (int row = 0; row < rows; row++) {
            if (!grid.rowContainsTopLeftCorner(row)) {
                // Just accumulate margins for the next column
                marginTop += grid.getRowHeight(row);
            } else {
                // Add horizontal attachments
                String firstId = null;
                for (View view : grid.viewsStartingInRow(row, true)) {
                    assert view.getId() != null;
                    if (firstId == null) {
                        firstId = view.getId();
                        if (view.isConstrainedVertically()) {
                            // Nothing to do -- we already have an accurate position for
                            // this view
                        } else if (attachTopProperty != null) {
                            view.addVerticalConstraint(attachTopProperty, attachTopValue);
                            if (marginTop > 0) {
                                view.addVerticalConstraint(ATTR_LAYOUT_MARGIN_TOP,
                                        String.format(VALUE_N_DP, marginTop));
                                marginTop = 0;
                            }
                        } else {
                            assert false;
                        }
                    } else if (!view.isConstrainedVertically()) {
                        view.addVerticalConstraint(ATTR_LAYOUT_ALIGN_TOP, firstId);
                    }
                }
            }

            // Figure out edge for the next row
            View view = grid.findBottomEdgeView(row);
            if (view != null) {
                assert view.getId() != null;
                attachTopProperty = ATTR_LAYOUT_BELOW;
                attachTopValue = view.getId();
                marginTop = 0;
            } else if (marginTop == 0) {
                marginTop = grid.getRowHeight(row);
            }
        }
    }

    /**
     * Searches a view hierarchy and locates the {@link CanvasViewInfo} for the given
     * {@link Element}
     *
     * @param info the root {@link CanvasViewInfo} to search below
     * @param element the target element
     * @return the {@link CanvasViewInfo} which corresponds to the given element
     */
    private CanvasViewInfo findViewForElement(CanvasViewInfo info, Element element) {
        if (getElement(info) == element) {
            return info;
        }

        for (CanvasViewInfo child : info.getChildren()) {
            CanvasViewInfo result = findViewForElement(child, element);
            if (result != null) {
                return result;
            }
        }

        return null;
    }

    /** Returns the {@link Element} for the given {@link CanvasViewInfo} */
    private static Element getElement(CanvasViewInfo info) {
        Node node = info.getUiViewNode().getXmlNode();
        if (node instanceof Element) {
            return (Element) node;
        }

        return null;
    }

    /**
     * A grid of cells which can contain views, used to infer spatial relationships when
     * computing constraints. Note that a view can appear in than one cell; they will
     * appear in all cells that their bounds overlap with!
     */
    private class Grid {
        private final int[] mLeft;
        private final int[] mTop;
        // A list from row to column to cell, where a cell is a list of views
        private final List<List<List<View>>> mRowList;
        private int mRowCount;
        private int mColCount;

        Grid(List<View> views, int[] left, int[] top) {
            mLeft = left;
            mTop = top;

            // The left/top arrays should include the ending point too
            mColCount = left.length - 1;
            mRowCount = top.length - 1;

            // Using nested lists rather than arrays to avoid lack of typed arrays
            // (can't create List<View>[row][column] arrays)
            mRowList = new ArrayList<List<List<View>>>(top.length);
            for (int row = 0; row < top.length; row++) {
                List<List<View>> columnList = new ArrayList<List<View>>(left.length);
                for (int col = 0; col < left.length; col++) {
                    columnList.add(new ArrayList<View>(4));
                }
                mRowList.add(columnList);
            }

            for (View view : views) {
                // Get rid of the root view; we don't want that in the attachments logic;
                // it was there originally such that it would contribute the outermost
                // edges.
                if (view.mElement == mLayout) {
                    continue;
                }

                for (int i = 0; i < view.mRowSpan; i++) {
                    for (int j = 0; j < view.mColSpan; j++) {
                        mRowList.get(view.mRow + i).get(view.mCol + j).add(view);
                    }
                }
            }
        }

        /**
         * Returns the number of rows in the grid
         *
         * @return the row count
         */
        public int getRows() {
            return mRowCount;
        }

        /**
         * Returns the number of columns in the grid
         *
         * @return the column count
         */
        public int getColumns() {
            return mColCount;
        }

        /**
         * Returns the list of views overlapping the given cell
         *
         * @param row the row of the target cell
         * @param col the column of the target cell
         * @return a list of views overlapping the given column
         */
        public List<View> get(int row, int col) {
            return mRowList.get(row).get(col);
        }

        /**
         * Returns true if the given column contains a top left corner of a view
         *
         * @param column the column to check
         * @return true if one or more views have their top left corner in this column
         */
        public boolean colContainsTopLeftCorner(int column) {
            for (int row = 0; row < mRowCount; row++) {
                View view = getTopLeftCorner(row, column);
                if (view != null) {
                    return true;
                }
            }

            return false;
        }

        /**
         * Returns true if the given row contains a top left corner of a view
         *
         * @param row the row to check
         * @return true if one or more views have their top left corner in this row
         */
        public boolean rowContainsTopLeftCorner(int row) {
            for (int col = 0; col < mColCount; col++) {
                View view = getTopLeftCorner(row, col);
                if (view != null) {
                    return true;
                }
            }

            return false;
        }

        /**
         * Returns a list of views (optionally sorted by increasing row index) that have
         * their left edge starting in the given column
         *
         * @param col the column to look up views for
         * @param sort whether to sort the result in increasing row order
         * @return a list of views starting in the given column
         */
        public List<View> viewsStartingInCol(int col, boolean sort) {
            List<View> views = new ArrayList<View>();
            for (int row = 0; row < mRowCount; row++) {
                View view = getTopLeftCorner(row, col);
                if (view != null) {
                    views.add(view);
                }
            }

            if (sort) {
                View.sortByRow(views);
            }

            return views;
        }

        /**
         * Returns a list of views (optionally sorted by increasing column index) that have
         * their top edge starting in the given row
         *
         * @param row the row to look up views for
         * @param sort whether to sort the result in increasing column order
         * @return a list of views starting in the given row
         */
        public List<View> viewsStartingInRow(int row, boolean sort) {
            List<View> views = new ArrayList<View>();
            for (int col = 0; col < mColCount; col++) {
                View view = getTopLeftCorner(row, col);
                if (view != null) {
                    views.add(view);
                }
            }

            if (sort) {
                View.sortByColumn(views);
            }

            return views;
        }

        /**
         * Returns the pixel width of the given column
         *
         * @param col the column to look up the width of
         * @return the width of the column
         */
        public int getColumnWidth(int col) {
            return mLeft[col + 1] - mLeft[col];
        }

        /**
         * Returns the pixel height of the given row
         *
         * @param row the row to look up the height of
         * @return the height of the row
         */
        public int getRowHeight(int row) {
            return mTop[row + 1] - mTop[row];
        }

        /**
         * Returns the first view found that has its top left corner in the cell given by
         * the row and column indexes, or null if not found.
         *
         * @param row the row of the target cell
         * @param col the column of the target cell
         * @return a view with its top left corner in the given cell, or null if not found
         */
        View getTopLeftCorner(int row, int col) {
            List<View> views = get(row, col);
            if (views.size() > 0) {
                for (View view : views) {
                    if (view.mRow == row && view.mCol == col) {
                        return view;
                    }
                }
            }

            return null;
        }

        public View findRightEdgeView(int col) {
            for (int row = 0; row < mRowCount; row++) {
                List<View> views = get(row, col);
                if (views.size() > 0) {
                    List<View> result = new ArrayList<View>();
                    for (View view : views) {
                        // Ends on the right edge of this column?
                        if (view.mCol + view.mColSpan == col + 1) {
                            result.add(view);
                        }
                    }
                    if (result.size() > 1) {
                        View.sortByColumn(result);
                    }
                    if (result.size() > 0) {
                        return result.get(0);
                    }
                }
            }

            return null;
        }

        public View findBottomEdgeView(int row) {
            for (int col = 0; col < mColCount; col++) {
                List<View> views = get(row, col);
                if (views.size() > 0) {
                    List<View> result = new ArrayList<View>();
                    for (View view : views) {
                        // Ends on the bottom edge of this column?
                        if (view.mRow + view.mRowSpan == row + 1) {
                            result.add(view);
                        }
                    }
                    if (result.size() > 1) {
                        View.sortByRow(result);
                    }
                    if (result.size() > 0) {
                        return result.get(0);
                    }

                }
            }

            return null;
        }

        /**
         * Produces a display of view contents along with the pixel positions of each row/column,
         * like the following (used for diagnostics only)
         * <pre>
         *          |0                  |49                 |143                |192           |240
         *        36|                   |                   |button2            |
         *        72|                   |radioButton1       |button2            |
         *        74|button1            |radioButton1       |button2            |
         *       108|button1            |                   |button2            |
         *       110|                   |                   |button2            |
         *       149|                   |                   |                   |
         *       320
         * </pre>
         */
        @Override
        public String toString() {
            // Dump out the view table
            int cellWidth = 20;

            StringWriter stringWriter = new StringWriter();
            PrintWriter out = new PrintWriter(stringWriter);
            out.printf("%" + cellWidth + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            for (int col = 0; col < mColCount + 1; col++) {
                out.printf("|%-" + (cellWidth - 1) + "d", mLeft[col]); //$NON-NLS-1$ //$NON-NLS-2$
            }
            out.printf("\n"); //$NON-NLS-1$
            for (int row = 0; row < mRowCount + 1; row++) {
                out.printf("%" + cellWidth + "d", mTop[row]); //$NON-NLS-1$ //$NON-NLS-2$
                if (row == mRowCount) {
                    break;
                }
                for (int col = 0; col < mColCount; col++) {
                    List<View> views = get(row, col);
                    StringBuilder sb = new StringBuilder();
                    for (View view : views) {
                        String id = view != null ? view.getId() : ""; //$NON-NLS-1$
                        if (id.startsWith(NEW_ID_PREFIX)) {
                            id = id.substring(NEW_ID_PREFIX.length());
                        }
                        if (id.length() > cellWidth - 2) {
                            id = id.substring(0, cellWidth - 2);
                        }
                        if (sb.length() > 0) {
                            sb.append(',');
                        }
                        sb.append(id);
                    }
                    String cellString = sb.toString();
                    if (cellString.contains(",") && cellString.length() > cellWidth - 2) { //$NON-NLS-1$
                        cellString = cellString.substring(0, cellWidth - 6) + "...,"; //$NON-NLS-1$
                    }
                    out.printf("|%-" + (cellWidth - 2) + "s ", cellString); //$NON-NLS-1$ //$NON-NLS-2$
                }
                out.printf("\n"); //$NON-NLS-1$
            }

            out.flush();
            return stringWriter.toString();
        }
    }

    /** Holds layout information about an individual view. */
    private static class View {
        private final Element mElement;
        private int mRow = -1;
        private int mCol = -1;
        private int mRowSpan = -1;
        private int mColSpan = -1;
        private CanvasViewInfo mInfo;
        private String mId;
        private List<Pair<String, String>> mHorizConstraints =
            new ArrayList<Pair<String, String>>(4);
        private List<Pair<String, String>> mVerticalConstraints =
            new ArrayList<Pair<String, String>>(4);
        private int mGravity;

        public View(CanvasViewInfo view, Element element) {
            mInfo = view;
            mElement = element;
            mGravity = RelativeLayoutConversionHelper.getGravity(element);
        }

        public int getHeight() {
            return mInfo.getAbsRect().height;
        }

        public int getGravity() {
            return mGravity;
        }

        public String getId() {
            return mId;
        }

        public Element getElement() {
            return mElement;
        }

        public List<Pair<String, String>> getHorizConstraints() {
            return mHorizConstraints;
        }

        public List<Pair<String, String>> getVerticalConstraints() {
            return mVerticalConstraints;
        }

        public boolean isConstrainedHorizontally() {
            return mHorizConstraints.size() > 0;
        }

        public boolean isConstrainedVertically() {
            return mVerticalConstraints.size() > 0;
        }

        public void addHorizConstraint(String property, String value) {
            assert property != null && value != null;
            // TODO - look for duplicates?
            mHorizConstraints.add(Pair.of(property, value));
        }

        public void addVerticalConstraint(String property, String value) {
            assert property != null && value != null;
            mVerticalConstraints.add(Pair.of(property, value));
        }

        public int getLeftEdge() {
            return mInfo.getAbsRect().x;
        }

        public int getTopEdge() {
            return mInfo.getAbsRect().y;
        }

        public int getRightEdge() {
            Rectangle bounds = mInfo.getAbsRect();
            // +1: make the bounds overlap, so the right edge is the same as the
            // left edge of the neighbor etc. Otherwise we end up with lots of 1-pixel wide
            // columns between adjacent items.
            return bounds.x + bounds.width + 1;
        }

        public int getBottomEdge() {
            Rectangle bounds = mInfo.getAbsRect();
            return bounds.y + bounds.height + 1;
        }

        @Override
        public String toString() {
            return "View [mId=" + mId + "]"; //$NON-NLS-1$ //$NON-NLS-2$
        }

        public static void sortByRow(List<View> views) {
            Collections.sort(views, new ViewComparator(true/*rowSort*/));
        }

        public static void sortByColumn(List<View> views) {
            Collections.sort(views, new ViewComparator(false/*rowSort*/));
        }

        /** Comparator to help sort views by row or column index */
        private static class ViewComparator implements Comparator<View> {
            boolean mRowSort;

            public ViewComparator(boolean rowSort) {
                mRowSort = rowSort;
            }

            public int compare(View view1, View view2) {
                if (mRowSort) {
                    return view1.mRow - view2.mRow;
                } else {
                    return view1.mCol - view2.mCol;
                }
            }
        }
    }

    /**
     * An edge list takes a hierarchy of elements and records the bounds of each element
     * into various lists such that it can answer queries about shared edges, about which
     * particular pixels occur as a boundary edge, etc.
     */
    private class EdgeList {
        private final Map<Element, View> mElementToViewMap = new HashMap<Element, View>(100);
        private final Map<String, View> mIdToViewMap = new HashMap<String, View>(100);
        private final Map<Integer, List<View>> mLeft = new HashMap<Integer, List<View>>();
        private final Map<Integer, List<View>> mTop = new HashMap<Integer, List<View>>();
        private final Map<Integer, List<View>> mRight = new HashMap<Integer, List<View>>();
        private final Map<Integer, List<View>> mBottom = new HashMap<Integer, List<View>>();
        private final Map<Element, Element> mSharedLeftEdge = new HashMap<Element, Element>();
        private final Map<Element, Element> mSharedTopEdge = new HashMap<Element, Element>();
        private final Map<Element, Element> mSharedRightEdge = new HashMap<Element, Element>();
        private final Map<Element, Element> mSharedBottomEdge = new HashMap<Element, Element>();
        private final List<Element> mDelete = new ArrayList<Element>();

        EdgeList(CanvasViewInfo view) {
            analyze(view, true);
            mDelete.remove(getElement(view));
        }

        public void setIdAttributeValue(View view, String id) {
            assert id.startsWith(NEW_ID_PREFIX) || id.startsWith(ID_PREFIX);
            view.mId = id;
            mIdToViewMap.put(getIdBasename(id), view);
        }

        public View getView(Element element) {
            return mElementToViewMap.get(element);
        }

        public View getView(String id) {
            return mIdToViewMap.get(id);
        }

        public List<View> getTopEdgeViews(Integer topOffset) {
            return mTop.get(topOffset);
        }

        public List<View> getLeftEdgeViews(Integer leftOffset) {
            return mLeft.get(leftOffset);
        }

        void record(Map<Integer, List<View>> map, Integer edge, View info) {
            List<View> list = map.get(edge);
            if (list == null) {
                list = new ArrayList<View>();
                map.put(edge, list);
            }
            list.add(info);
        }

        private List<Integer> getOffsets(Set<Integer> first, Set<Integer> second) {
            Set<Integer> joined = new HashSet<Integer>(first.size() + second.size());
            joined.addAll(first);
            joined.addAll(second);
            List<Integer> unique = new ArrayList<Integer>(joined);
            Collections.sort(unique);

            return unique;
        }

        public List<Element> getDeletedElements() {
            return mDelete;
        }

        public List<Integer> getColumnOffsets() {
            return getOffsets(mLeft.keySet(), mRight.keySet());
        }
        public List<Integer> getRowOffsets() {
            return getOffsets(mTop.keySet(), mBottom.keySet());
        }

        private View analyze(CanvasViewInfo view, boolean isRoot) {
            View added = null;
            if (!mFlatten || !isRemovableLayout(view)) {
                added = add(view);
                if (!isRoot) {
                    return added;
                }
            } else {
                mDelete.add(getElement(view));
            }

            Element parentElement = getElement(view);
            Rectangle parentBounds = view.getAbsRect();

            // Build up a table model of the view
            for (CanvasViewInfo child : view.getChildren()) {
                Rectangle childBounds = child.getAbsRect();
                Element childElement = getElement(child);

                // See if this view shares the edge with the removed
                // parent layout, and if so, record that such that we can
                // later handle attachments to the removed parent edges
                if (parentBounds.x == childBounds.x) {
                    mSharedLeftEdge.put(childElement, parentElement);
                }
                if (parentBounds.y == childBounds.y) {
                    mSharedTopEdge.put(childElement, parentElement);
                }
                if (parentBounds.x + parentBounds.width == childBounds.x + childBounds.width) {
                    mSharedRightEdge.put(childElement, parentElement);
                }
                if (parentBounds.y + parentBounds.height == childBounds.y + childBounds.height) {
                    mSharedBottomEdge.put(childElement, parentElement);
                }

                if (mFlatten && isRemovableLayout(child)) {
                    // When flattening, we want to disregard all layouts and instead
                    // add their children!
                    for (CanvasViewInfo childView : child.getChildren()) {
                        analyze(childView, false);

                        Element childViewElement = getElement(childView);
                        Rectangle childViewBounds = childView.getAbsRect();

                        // See if this view shares the edge with the removed
                        // parent layout, and if so, record that such that we can
                        // later handle attachments to the removed parent edges
                        if (parentBounds.x == childViewBounds.x) {
                            mSharedLeftEdge.put(childViewElement, parentElement);
                        }
                        if (parentBounds.y == childViewBounds.y) {
                            mSharedTopEdge.put(childViewElement, parentElement);
                        }
                        if (parentBounds.x + parentBounds.width == childViewBounds.x
                                + childViewBounds.width) {
                            mSharedRightEdge.put(childViewElement, parentElement);
                        }
                        if (parentBounds.y + parentBounds.height == childViewBounds.y
                                + childViewBounds.height) {
                            mSharedBottomEdge.put(childViewElement, parentElement);
                        }
                    }
                    mDelete.add(childElement);
                } else {
                    analyze(child, false);
                }
            }

            return added;
        }

        public View getSharedLeftEdge(Element element) {
            return getSharedEdge(element, mSharedLeftEdge);
        }

        public View getSharedRightEdge(Element element) {
            return getSharedEdge(element, mSharedRightEdge);
        }

        public View getSharedTopEdge(Element element) {
            return getSharedEdge(element, mSharedTopEdge);
        }

        public View getSharedBottomEdge(Element element) {
            return getSharedEdge(element, mSharedBottomEdge);
        }

        private View getSharedEdge(Element element, Map<Element, Element> sharedEdgeMap) {
            Element original = element;

            while (element != null) {
                View view = getView(element);
                if (view != null) {
                    assert isAncestor(element, original);
                    return view;
                }
                element = sharedEdgeMap.get(element);
            }

            return null;
        }

        private View add(CanvasViewInfo info) {
            Rectangle bounds = info.getAbsRect();
            Element element = getElement(info);
            View view = new View(info, element);
            mElementToViewMap.put(element, view);
            record(mLeft, Integer.valueOf(bounds.x), view);
            record(mTop, Integer.valueOf(bounds.y), view);
            record(mRight, Integer.valueOf(view.getRightEdge()), view);
            record(mBottom, Integer.valueOf(view.getBottomEdge()), view);
            return view;
        }

        /**
         * Returns true if the given {@link CanvasViewInfo} represents an element we
         * should remove in a flattening conversion. We don't want to remove non-layout
         * views, or layout views that for example contain drawables on their own.
         */
        private boolean isRemovableLayout(CanvasViewInfo child) {
            // The element being converted is NOT removable!
            Element element = getElement(child);
            if (element == mLayout) {
                return false;
            }

            ElementDescriptor descriptor = child.getUiViewNode().getDescriptor();
            String name = descriptor.getXmlLocalName();
            if (name.equals(LINEAR_LAYOUT) || name.equals(RELATIVE_LAYOUT)) {
                // Don't delete layouts that provide a background image or gradient
                if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
                    AdtPlugin.log(IStatus.WARNING,
                            "Did not flatten layout %1$s because it defines a '%2$s' attribute",
                            VisualRefactoring.getId(element), ATTR_BACKGROUND);
                    return false;
                }

                return true;
            }

            return false;
        }
    }
}
