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

import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_NAME_PREFIX;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.sdklib.SdkConstants;

import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.graphics.Image;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * {@link ElementDescriptor} describes the properties expected for a given XML element node.
 *
 * {@link ElementDescriptor} have an XML name, UI name, a tooltip, an SDK url,
 * an attributes list and a children list.
 *
 * An UI node can be "mandatory", meaning the UI node is never deleted and it may lack
 * an actual XML node attached. A non-mandatory UI node MUST have an XML node attached
 * and it will cease to exist when the XML node ceases to exist.
 */
public class ElementDescriptor implements Comparable<ElementDescriptor> {
    private static final String ELEMENT_ICON_FILENAME = "element"; //$NON-NLS-1$

    /** The XML element node name. Case sensitive. */
    protected final String mXmlName;
    /** The XML element name for the user interface, typically capitalized. */
    private final String mUiName;
    /** The list of allowed attributes. */
    private AttributeDescriptor[] mAttributes;
    /** The list of allowed children */
    private ElementDescriptor[] mChildren;
    /* An optional tooltip. Can be empty. */
    private String mTooltip;
    /** An optional SKD URL. Can be empty. */
    private String mSdkUrl;
    /** Whether this UI node must always exist (even for empty models). */
    private final Mandatory mMandatory;

    public enum Mandatory {
        NOT_MANDATORY,
        MANDATORY,
        MANDATORY_LAST
    }

    /**
     * Constructs a new {@link ElementDescriptor} based on its XML name, UI name,
     * tooltip, SDK url, attributes list, children list and mandatory.
     *
     * @param xml_name The XML element node name. Case sensitive.
     * @param ui_name The XML element name for the user interface, typically capitalized.
     * @param tooltip An optional tooltip. Can be null or empty.
     * @param sdk_url An optional SKD URL. Can be null or empty.
     * @param attributes The list of allowed attributes. Can be null or empty.
     * @param children The list of allowed children. Can be null or empty.
     * @param mandatory Whether this node must always exist (even for empty models). A mandatory
     *  UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
     *  UI node MUST have an XML node attached and it will cease to exist when the XML node
     *  ceases to exist.
     */
    public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
            AttributeDescriptor[] attributes,
            ElementDescriptor[] children,
            Mandatory mandatory) {
        mMandatory = mandatory;
        mXmlName = xml_name;
        mUiName = ui_name;
        mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
        mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null;
        setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{});
        mChildren = children != null ? children : new ElementDescriptor[]{};
    }

    /**
     * Constructs a new {@link ElementDescriptor} based on its XML name, UI name,
     * tooltip, SDK url, attributes list, children list and mandatory.
     *
     * @param xml_name The XML element node name. Case sensitive.
     * @param ui_name The XML element name for the user interface, typically capitalized.
     * @param tooltip An optional tooltip. Can be null or empty.
     * @param sdk_url An optional SKD URL. Can be null or empty.
     * @param attributes The list of allowed attributes. Can be null or empty.
     * @param children The list of allowed children. Can be null or empty.
     * @param mandatory Whether this node must always exist (even for empty models). A mandatory
     *  UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
     *  UI node MUST have an XML node attached and it will cease to exist when the XML node
     *  ceases to exist.
     */
    public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
            AttributeDescriptor[] attributes,
            ElementDescriptor[] children,
            boolean mandatory) {
        mMandatory = mandatory ? Mandatory.MANDATORY : Mandatory.NOT_MANDATORY;
        mXmlName = xml_name;
        mUiName = ui_name;
        mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
        mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null;
        setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{});
        mChildren = children != null ? children : new ElementDescriptor[]{};
    }

    /**
     * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
     * The UI name is build by capitalizing the XML name.
     * The UI nodes will be non-mandatory.
     *
     * @param xml_name The XML element node name. Case sensitive.
     * @param children The list of allowed children. Can be null or empty.
     * @param mandatory Whether this node must always exist (even for empty models). A mandatory
     *  UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
     *  UI node MUST have an XML node attached and it will cease to exist when the XML node
     *  ceases to exist.
     */
    public ElementDescriptor(String xml_name, ElementDescriptor[] children, Mandatory mandatory) {
        this(xml_name, prettyName(xml_name), null, null, null, children, mandatory);
    }

    /**
     * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
     * The UI name is build by capitalizing the XML name.
     * The UI nodes will be non-mandatory.
     *
     * @param xml_name The XML element node name. Case sensitive.
     * @param children The list of allowed children. Can be null or empty.
     */
    public ElementDescriptor(String xml_name, ElementDescriptor[] children) {
        this(xml_name, prettyName(xml_name), null, null, null, children, false);
    }

    /**
     * Constructs a new {@link ElementDescriptor} based on its XML name.
     * The UI name is build by capitalizing the XML name.
     * The UI nodes will be non-mandatory.
     *
     * @param xml_name The XML element node name. Case sensitive.
     */
    public ElementDescriptor(String xml_name) {
        this(xml_name, prettyName(xml_name), null, null, null, null, false);
    }

    /** Returns whether this node must always exist (even for empty models) */
    public Mandatory getMandatory() {
        return mMandatory;
    }

    @Override
    public String toString() {
        return String.format("%s [%s, attr %d, children %d%s]",    //$NON-NLS-1$
                this.getClass().getSimpleName(),
                mXmlName,
                mAttributes != null ? mAttributes.length : 0,
                mChildren != null ? mChildren.length : 0,
                mMandatory != Mandatory.NOT_MANDATORY ? ", " + mMandatory.toString() : "" //$NON-NLS-1$ //$NON-NLS-2$
                );
    }

    /**
     * Returns the XML element node local name (case sensitive)
     */
    public final String getXmlLocalName() {
        int pos = mXmlName.indexOf(':');
        if (pos != -1) {
            return mXmlName.substring(pos+1);
        }
        return mXmlName;
    }

    /**
     * Returns the XML element node name, including the prefix.
     * Case sensitive.
     * <p/>
     * In Android resources, the element node name for Android resources typically does not
     * have a prefix and is typically the simple Java class name (e.g. "View"), whereas for
     * custom views it is generally the fully qualified class name of the view (e.g.
     * "com.mycompany.myapp.MyView").
     * <p/>
     * Most of the time you'll probably want to use {@link #getXmlLocalName()} to get a local
     * name guaranteed without a prefix.
     * <p/>
     * Note that the prefix that <em>may</em> be available in this descriptor has nothing to
     * do with the actual prefix the node might have (or needs to have) in the actual XML file
     * since descriptors are fixed and do not depend on any current namespace defined in the
     * target XML.
     */
    public String getXmlName() {
        return mXmlName;
    }

    /**
     * Returns the namespace of the attribute.
     */
    public final String getNamespace() {
        // For now we hard-code the prefix as being "android"
        if (mXmlName.startsWith(ANDROID_NS_NAME_PREFIX)) {
            return SdkConstants.NS_RESOURCES;
        }

        return ""; //$NON-NLs-1$
    }


    /** Returns the XML element name for the user interface, typically capitalized. */
    public String getUiName() {
        return mUiName;
    }

    /**
     * Returns an icon for the element.
     * This icon is generic, that is all element descriptors have the same icon
     * no matter what they represent.
     *
     * @return An icon for this element or null.
     * @see #getCustomizedIcon()
     */
    public Image getGenericIcon() {
        return IconFactory.getInstance().getIcon(ELEMENT_ICON_FILENAME);
    }

    /**
     * Returns an optional icon for the element, typically to be used in XML form trees.
     * <p/>
     * This icon is customized to the given descriptor, that is different elements
     * will have different icons.
     * <p/>
     * By default this tries to return an icon based on the XML name of the element.
     * If this fails, it tries to return the default Android logo as defined in the
     * plugin. If all fails, it returns null.
     *
     * @return An icon for this element. This is never null.
     */
    public Image getCustomizedIcon() {
        IconFactory factory = IconFactory.getInstance();
        int color = hasChildren() ? IconFactory.COLOR_BLUE
                : IconFactory.COLOR_GREEN;
        int shape = hasChildren() ? IconFactory.SHAPE_RECT
                : IconFactory.SHAPE_CIRCLE;
        String name = mXmlName;

        int pos = name.lastIndexOf('.');
        if (pos != -1) {
            // If the user uses a fully qualified name, such as
            // "android.gesture.GestureOverlayView" in their XML, we need to
            // look up only by basename
            name = name.substring(pos + 1);
        }
        Image icon = factory.getIcon(name, color, shape);
        if (icon == null) {
            icon = getGenericIcon();
        }
        if (icon == null) {
            icon = AdtPlugin.getAndroidLogo();
        }
        return icon;
    }

    /**
     * Returns an optional ImageDescriptor for the element.
     * <p/>
     * By default this tries to return an image based on the XML name of the element.
     * If this fails, it tries to return the default Android logo as defined in the
     * plugin. If all fails, it returns null.
     *
     * @return An ImageDescriptor for this element or null.
     */
    public ImageDescriptor getImageDescriptor() {
        IconFactory factory = IconFactory.getInstance();
        int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN;
        int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE;
        ImageDescriptor id = factory.getImageDescriptor(mXmlName, color, shape);
        return id != null ? id : AdtPlugin.getAndroidLogoDesc();
    }

    /* Returns the list of allowed attributes. */
    public AttributeDescriptor[] getAttributes() {
        return mAttributes;
    }

    /** Sets the list of allowed attributes. */
    public void setAttributes(AttributeDescriptor[] attributes) {
        mAttributes = attributes;
        for (AttributeDescriptor attribute : attributes) {
            attribute.setParent(this);
        }
    }

    /** Returns the list of allowed children */
    public ElementDescriptor[] getChildren() {
        return mChildren;
    }

    /** @return True if this descriptor has children available */
    public boolean hasChildren() {
        return mChildren.length > 0;
    }

    /**
     * Checks whether this descriptor can accept the given descriptor type
     * as a direct child.
     *
     * @return True if this descriptor can accept children of the given descriptor type.
     *   False if not accepted, no children allowed, or target is null.
     */
    public boolean acceptChild(ElementDescriptor target) {
        if (target != null && mChildren.length > 0) {
            String targetXmlName = target.getXmlName();
            for (ElementDescriptor child : mChildren) {
                if (child.getXmlName().equals(targetXmlName)) {
                    return true;
                }
            }
        }

        return false;
    }

    /** Sets the list of allowed children. */
    public void setChildren(ElementDescriptor[] newChildren) {
        mChildren = newChildren;
    }

    /**
     * Sets the list of allowed children.
     * <p/>
     * This is just a convenience method that converts a Collection into an array and
     * calls {@link #setChildren(ElementDescriptor[])}.
     * <p/>
     * This means a <em>copy</em> of the collection is made. The collection is not
     * stored by the recipient and can thus be altered by the caller.
     */
    public void setChildren(Collection<ElementDescriptor> newChildren) {
        setChildren(newChildren.toArray(new ElementDescriptor[newChildren.size()]));
    }

    /**
     * Returns an optional tooltip. Will be null if not present.
     * <p/>
     * The tooltip is based on the Javadoc of the element and already processed via
     * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as
     * a UI tooltip.
     */
    public String getTooltip() {
        return mTooltip;
    }

    /** Returns an optional SKD URL. Will be null if not present. */
    public String getSdkUrl() {
        return mSdkUrl;
    }

    /** Sets the optional tooltip. Can be null or empty. */
    public void setTooltip(String tooltip) {
        mTooltip = tooltip;
    }

    /** Sets the optional SDK URL. Can be null or empty. */
    public void setSdkUrl(String sdkUrl) {
        mSdkUrl = sdkUrl;
    }

    /**
     * @return A new {@link UiElementNode} linked to this descriptor.
     */
    public UiElementNode createUiNode() {
        return new UiElementNode(this);
    }

    /**
     * Returns the first children of this descriptor that describes the given XML element name.
     * <p/>
     * In recursive mode, searches the direct children first before descending in the hierarchy.
     *
     * @return The ElementDescriptor matching the requested XML node element name or null.
     */
    public ElementDescriptor findChildrenDescriptor(String element_name, boolean recursive) {
        return findChildrenDescriptorInternal(element_name, recursive, null);
    }

    private ElementDescriptor findChildrenDescriptorInternal(String element_name,
            boolean recursive,
            Set<ElementDescriptor> visited) {
        if (recursive && visited == null) {
            visited = new HashSet<ElementDescriptor>();
        }

        for (ElementDescriptor e : getChildren()) {
            if (e.getXmlName().equals(element_name)) {
                return e;
            }
        }

        if (visited != null) {
            visited.add(this);
        }

        if (recursive) {
            for (ElementDescriptor e : getChildren()) {
                if (visited != null) {
                    if (!visited.add(e)) {  // Set.add() returns false if element is already present
                        continue;
                    }
                }
                ElementDescriptor f = e.findChildrenDescriptorInternal(element_name,
                        recursive, visited);
                if (f != null) {
                    return f;
                }
            }
        }

        return null;
    }

    /**
     * Utility helper than pretty-formats an XML Name for the UI.
     * This is used by the simplified constructor that takes only an XML element name.
     *
     * @param xml_name The XML name to convert.
     * @return The XML name with dashes replaced by spaces and capitalized.
     */
    private static String prettyName(String xml_name) {
        char c[] = xml_name.toCharArray();
        if (c.length > 0) {
            c[0] = Character.toUpperCase(c[0]);
        }
        return new String(c).replace("-", " ");  //$NON-NLS-1$  //$NON-NLS-2$
    }

    /**
     * Returns true if this node defines the given attribute
     *
     * @param namespaceUri the namespace URI of the target attribute
     * @param attributeName the attribute name
     * @return true if this element defines an attribute of the given name and namespace
     */
    public boolean definesAttribute(String namespaceUri, String attributeName) {
        for (AttributeDescriptor desc : mAttributes) {
            if (desc.getXmlLocalName().equals(attributeName) &&
                    desc.getNamespaceUri().equals(namespaceUri)) {
                return true;
            }
        }

        return false;
    }

    // Implements Comparable<ElementDescriptor>:
    public int compareTo(ElementDescriptor o) {
        return mUiName.compareToIgnoreCase(o.mUiName);
    }
}
