/*
 * Copyright (C) 2009 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.gre;

import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.INodeHandler;
import com.android.ide.common.api.Margins;
import com.android.ide.common.api.Rect;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleAttribute;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;

import org.eclipse.swt.graphics.Rectangle;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 */
public class NodeProxy implements INode {
    private static final Margins NO_MARGINS = new Margins(0, 0, 0, 0);
    private final UiViewElementNode mNode;
    private final Rect mBounds;
    private final NodeFactory mFactory;
    /** Map from URI to Map(key=>value) (where no namespace uses "" as a key) */
    private Map<String, Map<String, String>> mPendingAttributes;

    /**
     * Creates a new {@link INode} that wraps an {@link UiViewElementNode} that is
     * actually valid in the current UI/XML model. The view may not be part of the canvas
     * yet (e.g. if it has just been dynamically added and the canvas hasn't reloaded yet.)
     * <p/>
     * This method is package protected. To create a node, please use {@link NodeFactory} instead.
     *
     * @param uiNode The node to wrap.
     * @param bounds The bounds of a the view in the canvas. Must be either: <br/>
     *   - a valid rect for a view that is actually in the canvas <br/>
     *   - <b>*or*</b> null (or an invalid rect) for a view that has just been added dynamically
     *   to the model. We never store a null bounds rectangle in the node, a null rectangle
     *   will be converted to an invalid rectangle.
     * @param factory A {@link NodeFactory} to create unique children nodes.
     */
    /*package*/ NodeProxy(UiViewElementNode uiNode, Rectangle bounds, NodeFactory factory) {
        mNode = uiNode;
        mFactory = factory;
        if (bounds == null) {
            mBounds = new Rect();
        } else {
            mBounds = SwtUtils.toRect(bounds);
        }
    }

    public Rect getBounds() {
        return mBounds;
    }

    public Margins getMargins() {
        ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
        CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
        if (view != null) {
            return view.getMargins();
        }

        return NO_MARGINS;
    }


    public int getBaseline() {
        ViewHierarchy viewHierarchy = mFactory.getCanvas().getViewHierarchy();
        CanvasViewInfo view = viewHierarchy.findViewInfoFor(this);
        if (view != null) {
            return view.getBaseline();
        }

        return -1;
    }

    /**
     * Updates the bounds of this node proxy. Bounds cannot be null, but it can be invalid.
     * This is a package-protected method, only the {@link NodeFactory} uses this method.
     */
    /*package*/ void setBounds(Rectangle bounds) {
        SwtUtils.set(mBounds, bounds);
    }

    /**
     * Returns the {@link UiViewElementNode} corresponding to this
     * {@link NodeProxy}.
     *
     * @return The {@link UiViewElementNode} corresponding to this
     *         {@link NodeProxy}
     */
    public UiViewElementNode getNode() {
        return mNode;
    }

    public String getFqcn() {
        if (mNode != null) {
            ElementDescriptor desc = mNode.getDescriptor();
            if (desc instanceof ViewElementDescriptor) {
                return ((ViewElementDescriptor) desc).getFullClassName();
            }
        }
        return null;
    }


    // ---- Hierarchy handling ----


    public INode getRoot() {
        if (mNode != null) {
            UiElementNode p = mNode.getUiRoot();
            // The node root should be a document. Instead what we really mean to
            // return is the top level view element.
            if (p instanceof UiDocumentNode) {
                List<UiElementNode> children = p.getUiChildren();
                if (children.size() > 0) {
                    p = children.get(0);
                }
            }

            // Cope with a badly structured XML layout
            while (p != null && !(p instanceof UiViewElementNode)) {
                p = p.getUiNextSibling();
            }

            if (p == mNode) {
                return this;
            }
            if (p instanceof UiViewElementNode) {
                return mFactory.create((UiViewElementNode) p);
            }
        }

        return null;
    }

    public INode getParent() {
        if (mNode != null) {
            UiElementNode p = mNode.getUiParent();
            if (p instanceof UiViewElementNode) {
                return mFactory.create((UiViewElementNode) p);
            }
        }

        return null;
    }

    public INode[] getChildren() {
        if (mNode != null) {
            ArrayList<INode> nodes = new ArrayList<INode>();
            for (UiElementNode uiChild : mNode.getUiChildren()) {
                if (uiChild instanceof UiViewElementNode) {
                    nodes.add(mFactory.create((UiViewElementNode) uiChild));
                }
            }

            return nodes.toArray(new INode[nodes.size()]);
        }

        return new INode[0];
    }


    // ---- XML Editing ---

    public void editXml(String undoName, final INodeHandler c) {
        final AndroidXmlEditor editor = mNode.getEditor();

        if (editor instanceof LayoutEditor) {
            // Create an undo edit XML wrapper, which takes a runnable
            ((LayoutEditor) editor).wrapUndoEditXmlModel(
                    undoName,
                    new Runnable() {
                        public void run() {
                            // Here editor.isEditXmlModelPending returns true and it
                            // is safe to edit the model using any method from INode.

                            // Finally execute the closure that will act on the XML
                            c.handle(NodeProxy.this);
                            applyPendingChanges();
                        }
                    });
        }
    }

    private void checkEditOK() {
        final AndroidXmlEditor editor = mNode.getEditor();
        if (!editor.isEditXmlModelPending()) {
            throw new RuntimeException("Error: XML edit call without using INode.editXml!");
        }
    }

    public INode appendChild(String viewFqcn) {
        return insertOrAppend(viewFqcn, -1);
    }

    public INode insertChildAt(String viewFqcn, int index) {
        return insertOrAppend(viewFqcn, index);
    }

    public void removeChild(INode node) {
        checkEditOK();

        ((NodeProxy) node).mNode.deleteXmlNode();
    }

    private INode insertOrAppend(String viewFqcn, int index) {
        checkEditOK();

        // Find the descriptor for this FQCN
        ViewElementDescriptor vd = getFqcnViewDescriptor(viewFqcn);
        if (vd == null) {
            warnPrintf("Can't create a new %s element", viewFqcn);
            return null;
        }

        final UiElementNode uiNew;
        if (index == -1) {
            // Append at the end.
            uiNew = mNode.appendNewUiChild(vd);
        } else {
            // Insert at the requested position or at the end.
            int n = mNode.getUiChildren().size();
            if (index < 0 || index >= n) {
                uiNew = mNode.appendNewUiChild(vd);
            } else {
                uiNew = mNode.insertNewUiChild(index, vd);
            }
        }

        RulesEngine engine = null;
        AndroidXmlEditor editor = mNode.getEditor();
        if (editor instanceof LayoutEditor) {
            engine = ((LayoutEditor)editor).getRulesEngine();
        }

        // Set default attributes -- but only for new widgets (not when moving or copying)
        if (engine == null || engine.getInsertType().isCreate()) {
            // TODO: This should probably use IViewRule#getDefaultAttributes() at some point
            DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
        }

        Node xmlNode = uiNew.createXmlNode();

        if (!(uiNew instanceof UiViewElementNode) || xmlNode == null) {
            // Both things are not supposed to happen. When they do, we're in big trouble.
            // We don't really know how to revert the state at this point and the UI model is
            // now out of sync with the XML model.
            // Panic ensues.
            // The best bet is to abort now. The edit wrapper will release the edit and the
            // XML/UI should get reloaded properly (with a likely invalid XML.)
            warnPrintf("Failed to create a new %s element", viewFqcn);
            throw new RuntimeException("XML node creation failed."); //$NON-NLS-1$
        }

        UiViewElementNode uiNewView = (UiViewElementNode) uiNew;
        NodeProxy newNode = mFactory.create(uiNewView);

        if (engine != null) {
            engine.callCreateHooks(editor, this, newNode, null);
        }

        return newNode;
    }

    public boolean setAttribute(String uri, String name, String value) {
        checkEditOK();
        UiAttributeNode attr = mNode.setAttributeValue(name, uri, value, true /* override */);

        if (uri == null) {
            uri = ""; //$NON-NLS-1$
        }

        Map<String, String> map = null;
        if (mPendingAttributes == null) {
            // Small initial size: we don't expect many different namespaces
            mPendingAttributes = new HashMap<String, Map<String, String>>(3);
        } else {
            map = mPendingAttributes.get(uri);
        }
        if (map == null) {
            map = new HashMap<String, String>();
            mPendingAttributes.put(uri, map);
        }
        map.put(name, value);

        return attr != null;
    }

    public String getStringAttr(String uri, String attrName) {
        UiElementNode uiNode = mNode;

        if (attrName == null) {
            return null;
        }

        if (mPendingAttributes != null) {
            Map<String, String> map = mPendingAttributes.get(uri == null ? "" : uri); //$NON-NLS-1$
            if (map != null) {
                String value = map.get(attrName);
                if (value != null) {
                    return value;
                }
            }
        }

        if (uiNode.getXmlNode() != null) {
            Node xmlNode = uiNode.getXmlNode();
            if (xmlNode != null) {
                NamedNodeMap nodeAttributes = xmlNode.getAttributes();
                if (nodeAttributes != null) {
                    Node attr = nodeAttributes.getNamedItemNS(uri, attrName);
                    if (attr != null) {
                        return attr.getNodeValue();
                    }
                }
            }
        }
        return null;
    }

    public IAttributeInfo getAttributeInfo(String uri, String attrName) {
        UiElementNode uiNode = mNode;

        if (attrName == null) {
            return null;
        }

        for (AttributeDescriptor desc : uiNode.getAttributeDescriptors()) {
            String dUri = desc.getNamespaceUri();
            String dName = desc.getXmlLocalName();
            if ((uri == null && dUri == null) || (uri != null && uri.equals(dUri))) {
                if (attrName.equals(dName)) {
                    return desc.getAttributeInfo();
                }
            }
        }

        return null;
    }

    public IAttributeInfo[] getDeclaredAttributes() {

        AttributeDescriptor[] descs = mNode.getAttributeDescriptors();
        int n = descs.length;
        IAttributeInfo[] infos = new AttributeInfo[n];

        for (int i = 0; i < n; i++) {
            infos[i] = descs[i].getAttributeInfo();
        }

        return infos;
    }

    public List<String> getAttributeSources() {
        ElementDescriptor descriptor = mNode.getDescriptor();
        if (descriptor instanceof ViewElementDescriptor) {
            return ((ViewElementDescriptor) descriptor).getAttributeSources();
        } else {
            return Collections.emptyList();
        }
    }

    public IAttribute[] getLiveAttributes() {
        UiElementNode uiNode = mNode;

        if (uiNode.getXmlNode() != null) {
            Node xmlNode = uiNode.getXmlNode();
            if (xmlNode != null) {
                NamedNodeMap nodeAttributes = xmlNode.getAttributes();
                if (nodeAttributes != null) {

                    int n = nodeAttributes.getLength();
                    IAttribute[] result = new IAttribute[n];
                    for (int i = 0; i < n; i++) {
                        Node attr = nodeAttributes.item(i);
                        String uri = attr.getNamespaceURI();
                        String name = attr.getLocalName();
                        String value = attr.getNodeValue();

                        result[i] = new SimpleAttribute(uri, name, value);
                    }
                    return result;
                }
            }
        }
        return null;

    }

    @Override
    public String toString() {
        return "NodeProxy [node=" + mNode + ", bounds=" + mBounds + "]";
    }

    // --- internal helpers ---

    /**
     * Helper methods that returns a {@link ViewElementDescriptor} for the requested FQCN.
     * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info
     * (which shouldn't really happen since at this point the SDK should be fully loaded and
     * isn't reloading, or we wouldn't be here editing XML for a layout rule.)
     */
    private ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
        AndroidXmlEditor editor = mNode.getEditor();
        if (editor instanceof LayoutEditor) {
            return ((LayoutEditor) editor).getFqcnViewDescriptor(fqcn);
        }

        return null;
    }

    private void warnPrintf(String msg, Object...params) {
        AdtPlugin.printToConsole(
                mNode == null ? "" : mNode.getDescriptor().getXmlLocalName(),
                String.format(msg, params)
                );
    }

    /**
     * If there are any pending changes in these nodes, apply them now
     *
     * @return true if any modifications were made
     */
    public boolean applyPendingChanges() {
        boolean modified = false;

        // Flush all pending attributes
        if (mPendingAttributes != null) {
            mNode.commitDirtyAttributesToXml();
            modified = true;
            mPendingAttributes = null;

        }
        for (INode child : getChildren()) {
            modified |= ((NodeProxy) child).applyPendingChanges();
        }

        return modified;
    }
}
