/*
 * 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.manifest.descriptors;

import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.layout.LayoutConstants;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.common.resources.platform.AttrsXmlParser;
import com.android.ide.common.resources.platform.DeclareStyleableInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
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.descriptors.ElementDescriptor.Mandatory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ITextAttributeCreator;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ListAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.runtime.IStatus;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;


/**
 * Complete description of the AndroidManifest.xml structure.
 * <p/>
 * The root element are static instances which always exists.
 * However their sub-elements and attributes are created only when the SDK changes or is
 * loaded the first time.
 */
public final class AndroidManifestDescriptors implements IDescriptorProvider {

    private static final String MANIFEST_NODE_NAME = "manifest";                //$NON-NLS-1$
    private static final String ANDROID_MANIFEST_STYLEABLE =
        AttrsXmlParser.ANDROID_MANIFEST_STYLEABLE;

    // Public attributes names, attributes descriptors and elements descriptors

    public static final String ANDROID_LABEL_ATTR = "label";    //$NON-NLS-1$
    public static final String ANDROID_NAME_ATTR  = "name";     //$NON-NLS-1$
    public static final String PACKAGE_ATTR       = "package";  //$NON-NLS-1$

    /** The {@link ElementDescriptor} for the root Manifest element. */
    private final ElementDescriptor MANIFEST_ELEMENT;
    /** The {@link ElementDescriptor} for the root Application element. */
    private final ElementDescriptor APPLICATION_ELEMENT;

    /** The {@link ElementDescriptor} for the root Instrumentation element. */
    private final ElementDescriptor INTRUMENTATION_ELEMENT;
    /** The {@link ElementDescriptor} for the root Permission element. */
    private final ElementDescriptor PERMISSION_ELEMENT;
    /** The {@link ElementDescriptor} for the root UsesPermission element. */
    private final ElementDescriptor USES_PERMISSION_ELEMENT;
    /** The {@link ElementDescriptor} for the root UsesSdk element. */
    private final ElementDescriptor USES_SDK_ELEMENT;

    /** The {@link ElementDescriptor} for the root PermissionGroup element. */
    private final ElementDescriptor PERMISSION_GROUP_ELEMENT;
    /** The {@link ElementDescriptor} for the root PermissionTree element. */
    private final ElementDescriptor PERMISSION_TREE_ELEMENT;

    /** Private package attribute for the manifest element. Needs to be handled manually. */
    private final TextAttributeDescriptor PACKAGE_ATTR_DESC;

    public AndroidManifestDescriptors() {
        APPLICATION_ELEMENT = createElement("application", null, Mandatory.MANDATORY_LAST); //$NON-NLS-1$ + no child & mandatory
        INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$

        PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$
        USES_PERMISSION_ELEMENT = createElement("uses-permission"); //$NON-NLS-1$
        USES_SDK_ELEMENT = createElement("uses-sdk", null, Mandatory.MANDATORY); //$NON-NLS-1$ + no child & mandatory

        PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$
        PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$

        MANIFEST_ELEMENT = createElement(
                        MANIFEST_NODE_NAME, // xml name
                        new ElementDescriptor[] {
                                        APPLICATION_ELEMENT,
                                        INTRUMENTATION_ELEMENT,
                                        PERMISSION_ELEMENT,
                                        USES_PERMISSION_ELEMENT,
                                        PERMISSION_GROUP_ELEMENT,
                                        PERMISSION_TREE_ELEMENT,
                                        USES_SDK_ELEMENT,
                        },
                        Mandatory.MANDATORY);

        // The "package" attribute is treated differently as it doesn't have the standard
        // Android XML namespace.
        PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR,
                "Package",
                null /* nsUri */,
                "This attribute gives a unique name for the package, using a Java-style naming convention to avoid name collisions.\nFor example, applications published by Google could have names of the form com.google.app.appname",
                new AttributeInfo(PACKAGE_ATTR, new Format[] { Format.REFERENCE }) );
    }

    public ElementDescriptor[] getRootElementDescriptors() {
        return new ElementDescriptor[] { MANIFEST_ELEMENT };
    }

    public ElementDescriptor getDescriptor() {
        return getManifestElement();
    }

    public ElementDescriptor getApplicationElement() {
        return APPLICATION_ELEMENT;
    }

    public ElementDescriptor getManifestElement() {
        return MANIFEST_ELEMENT;
    }

    public ElementDescriptor getUsesSdkElement() {
        return USES_SDK_ELEMENT;
    }

    public ElementDescriptor getInstrumentationElement() {
        return INTRUMENTATION_ELEMENT;
    }

    public ElementDescriptor getPermissionElement() {
        return PERMISSION_ELEMENT;
    }

    public ElementDescriptor getUsesPermissionElement() {
        return USES_PERMISSION_ELEMENT;
    }

    public ElementDescriptor getPermissionGroupElement() {
        return PERMISSION_GROUP_ELEMENT;
    }

    public ElementDescriptor getPermissionTreeElement() {
        return PERMISSION_TREE_ELEMENT;
    }

    /**
     * Updates the document descriptor.
     * <p/>
     * It first computes the new children of the descriptor and then updates them
     * all at once.
     *
     * @param manifestMap The map style => attributes from the attrs_manifest.xml file
     */
    public synchronized void updateDescriptors(
            Map<String, DeclareStyleableInfo> manifestMap) {

        // -- setup the required attributes overrides --

        Set<String> required = new HashSet<String>();
        required.add("provider/authorities");  //$NON-NLS-1$

        // -- setup the various attribute format overrides --

        // The key for each override is "element1,element2,.../attr-xml-local-name" or
        // "*/attr-xml-local-name" to match the attribute in any element.

        Map<String, ITextAttributeCreator> overrides = new HashMap<String, ITextAttributeCreator>();

        overrides.put("*/icon",             ReferenceAttributeDescriptor.CREATOR);  //$NON-NLS-1$

        overrides.put("*/theme",            ThemeAttributeDescriptor.CREATOR);      //$NON-NLS-1$
        overrides.put("*/permission",       ListAttributeDescriptor.CREATOR);       //$NON-NLS-1$
        overrides.put("*/targetPackage",    ManifestPkgAttrDescriptor.CREATOR);     //$NON-NLS-1$

        overrides.put("uses-library/name",  ListAttributeDescriptor.CREATOR);       //$NON-NLS-1$
        overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR,       //$NON-NLS-1$
                                            ListAttributeDescriptor.CREATOR);

        overrides.put("application/" + ANDROID_NAME_ATTR,                           //$NON-NLS-1$
                                            ApplicationAttributeDescriptor.CREATOR);

        overrideClassName(overrides, "activity", SdkConstants.CLASS_ACTIVITY);           //$NON-NLS-1$
        overrideClassName(overrides, "receiver", SdkConstants.CLASS_BROADCASTRECEIVER);  //$NON-NLS-1$
        overrideClassName(overrides, "service",  SdkConstants.CLASS_SERVICE);            //$NON-NLS-1$
        overrideClassName(overrides, "provider", SdkConstants.CLASS_CONTENTPROVIDER);    //$NON-NLS-1$
        overrideClassName(overrides, "instrumentation", SdkConstants.CLASS_INSTRUMENTATION);    //$NON-NLS-1$

        // -- list element nodes already created --
        // These elements are referenced by already opened editors, so we want to update them
        // but not re-create them when reloading an SDK on the fly.

        HashMap<String, ElementDescriptor> elementDescs =
            new HashMap<String, ElementDescriptor>();
        elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(),         MANIFEST_ELEMENT);
        elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(),      APPLICATION_ELEMENT);
        elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(),   INTRUMENTATION_ELEMENT);
        elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(),       PERMISSION_ELEMENT);
        elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(),  USES_PERMISSION_ELEMENT);
        elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(),         USES_SDK_ELEMENT);
        elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT);
        elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(),  PERMISSION_TREE_ELEMENT);

        // --

        inflateElement(manifestMap,
                overrides,
                required,
                elementDescs,
                MANIFEST_ELEMENT,
                "AndroidManifest"); //$NON-NLS-1$
        insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC);

        XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(
                LayoutConstants.ANDROID_NS_NAME, SdkConstants.NS_RESOURCES);
        insertAttribute(MANIFEST_ELEMENT, xmlns);

        assert sanityCheck(manifestMap, MANIFEST_ELEMENT);
    }

    /**
     * Sets up an attribute override for ANDROID_NAME_ATTR using a ClassAttributeDescriptor
     * with the specified class name.
     */
    private static void overrideClassName(
            Map<String, ITextAttributeCreator> overrides,
            String elementName,
            final String className) {
        overrides.put(elementName + "/" + ANDROID_NAME_ATTR,
                new ITextAttributeCreator() {
            public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri,
                    String tooltip, IAttributeInfo attrInfo) {
                uiName += "*";  //$NON-NLS-1$

                if (attrInfo == null) {
                    attrInfo = new AttributeInfo(xmlName, new Format[] { Format.STRING } );
                }

                if (SdkConstants.CLASS_ACTIVITY.equals(className)) {
                    return new ClassAttributeDescriptor(
                            className,
                            PostActivityCreationAction.getAction(),
                            xmlName,
                            uiName,
                            nsUri,
                            tooltip,
                            attrInfo,
                            true /*mandatory */,
                            true /*defaultToProjectOnly*/);
                } else if (SdkConstants.CLASS_BROADCASTRECEIVER.equals(className)) {
                    return new ClassAttributeDescriptor(
                            className,
                            PostReceiverCreationAction.getAction(),
                            xmlName,
                            uiName,
                            nsUri,
                            tooltip,
                            attrInfo,
                            true /*mandatory */,
                            true /*defaultToProjectOnly*/);
                } else if (SdkConstants.CLASS_INSTRUMENTATION.equals(className)) {
                    return new ClassAttributeDescriptor(
                            className,
                            null, // no post action
                            xmlName,
                            uiName,
                            nsUri,
                            tooltip,
                            attrInfo,
                            true /*mandatory */,
                            false /*defaultToProjectOnly*/);
                } else {
                    return new ClassAttributeDescriptor(
                            className,
                            xmlName,
                            uiName,
                            nsUri,
                            tooltip,
                            attrInfo,
                            true /*mandatory */);
                }
            }
        });
    }

    /**
     * Returns a new ElementDescriptor constructed from the information given here
     * and the javadoc & attributes extracted from the style map if any.
     * <p/>
     * Creates an element with no attribute overrides.
     */
    private ElementDescriptor createElement(
            String xmlName,
            ElementDescriptor[] childrenElements,
            Mandatory mandatory) {
        // Creates an element with no attribute overrides.
        String styleName = guessStyleName(xmlName);
        String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName;
        String uiName = getUiName(xmlName);

        ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl,
                null, childrenElements, mandatory);

        return element;
    }

    /**
     * Returns a new ElementDescriptor constructed from its XML local name.
     * <p/>
     * This version creates an element not mandatory.
     */
    private ElementDescriptor createElement(String xmlName) {
        // Creates an element with no child and not mandatory
        return createElement(xmlName, null, Mandatory.NOT_MANDATORY);
    }

    /**
     * Inserts an attribute in this element attribute list if it is not present there yet
     * (based on the attribute XML name.)
     * The attribute is inserted at the beginning of the attribute list.
     */
    private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) {
        AttributeDescriptor[] attributes = element.getAttributes();
        for (AttributeDescriptor attr : attributes) {
            if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) {
                return;
            }
        }

        AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1];
        newArray[0] = newAttr;
        System.arraycopy(attributes, 0, newArray, 1, attributes.length);
        element.setAttributes(newArray);
    }

    /**
     * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration.
     * <p/>
     * This first creates all the attributes for the given ElementDescriptor.
     * It then finds all children of the descriptor, inflates them recursively and sets them
     * as child to this ElementDescriptor.
     *
     * @param styleMap The input styleable map for manifest elements & attributes.
     * @param overrides A list of attribute overrides (to customize the type of the attribute
     *          descriptors).
     * @param requiredAttributes Set of attributes to be marked as required.
     * @param existingElementDescs A map of already created element descriptors, keyed by
     *          XML local name. This is used to use the static elements created initially by this
     *          class, which are referenced directly by editors (so that reloading an SDK won't
     *          break these references).
     * @param elemDesc The current {@link ElementDescriptor} to inflate.
     * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name
     *          will be guessed automatically from the style name.
     */
    private void inflateElement(
            Map<String, DeclareStyleableInfo> styleMap,
            Map<String, ITextAttributeCreator> overrides,
            Set<String> requiredAttributes,
            HashMap<String, ElementDescriptor> existingElementDescs,
            ElementDescriptor elemDesc,
            String styleName) {
        assert elemDesc != null;
        assert styleName != null;
        assert styleMap != null;

        if (styleMap == null) {
            return;
        }

        // define attributes
        DeclareStyleableInfo style = styleMap.get(styleName);
        if (style != null) {
            ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>();
            DescriptorsUtils.appendAttributes(attrDescs,
                    elemDesc.getXmlLocalName(),
                    SdkConstants.NS_RESOURCES,
                    style.getAttributes(),
                    requiredAttributes,
                    overrides);
            elemDesc.setTooltip(style.getJavaDoc());
            elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()]));
        }

        // find all elements that have this one as parent
        ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>();
        for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) {
            DeclareStyleableInfo childStyle = entry.getValue();
            boolean isParent = false;
            String[] parents = childStyle.getParents();
            if (parents != null) {
                for (String parent: parents) {
                    if (styleName.equals(parent)) {
                        isParent = true;
                        break;
                    }
                }
            }
            if (isParent) {
                String childStyleName = entry.getKey();
                String childXmlName = guessXmlName(childStyleName);

                // create or re-use element
                ElementDescriptor child = existingElementDescs.get(childXmlName);
                if (child == null) {
                    child = createElement(childXmlName);
                    existingElementDescs.put(childXmlName, child);
                }
                children.add(child);

                inflateElement(styleMap,
                        overrides,
                        requiredAttributes,
                        existingElementDescs,
                        child,
                        childStyleName);
            }
        }
        elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()]));
    }

    /**
     * Get an UI name from the element XML name.
     * <p/>
     * Capitalizes the first letter and replace non-alphabet by a space followed by a capital.
     */
    private static String getUiName(String xmlName) {
        StringBuilder sb = new StringBuilder();

        boolean capitalize = true;
        for (char c : xmlName.toCharArray()) {
            if (capitalize && c >= 'a' && c <= 'z') {
                sb.append((char)(c + 'A' - 'a'));
                capitalize = false;
            } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
                sb.append(' ');
                capitalize = true;
            } else {
                sb.append(c);
            }
        }

        return sb.toString();
    }

    /**
     * Guesses the style name for a given XML element name.
     * <p/>
     * The rules are:
     * - capitalize the first letter:
     * - if there's a dash, skip it and capitalize the next one
     * - prefix AndroidManifest
     * The exception is "manifest" which just becomes AndroidManifest.
     * <p/>
     * Examples:
     * - manifest        => AndroidManifest
     * - application     => AndroidManifestApplication
     * - uses-permission => AndroidManifestUsesPermission
     */
    private String guessStyleName(String xmlName) {
        StringBuilder sb = new StringBuilder();

        if (!xmlName.equals(MANIFEST_NODE_NAME)) {
            boolean capitalize = true;
            for (char c : xmlName.toCharArray()) {
                if (capitalize && c >= 'a' && c <= 'z') {
                    sb.append((char)(c + 'A' - 'a'));
                    capitalize = false;
                } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
                    // not a letter -- skip the character and capitalize the next one
                    capitalize = true;
                } else {
                    sb.append(c);
                }
            }
        }

        sb.insert(0, ANDROID_MANIFEST_STYLEABLE);
        return sb.toString();
    }

    /**
     * This method performs a sanity check to make sure all the styles declared in the
     * manifestMap are actually defined in the actual element descriptors and reachable from
     * the manifestElement root node.
     */
    private boolean sanityCheck(Map<String, DeclareStyleableInfo> manifestMap,
            ElementDescriptor manifestElement) {
        TreeSet<String> elementsDeclared = new TreeSet<String>();
        findAllElementNames(manifestElement, elementsDeclared);

        TreeSet<String> stylesDeclared = new TreeSet<String>();
        for (String styleName : manifestMap.keySet()) {
            if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) {
                stylesDeclared.add(styleName);
            }
        }

        for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) {
            String xmlName = it.next();
            String styleName = guessStyleName(xmlName);
            if (stylesDeclared.remove(styleName)) {
                it.remove();
            }
        }

        StringBuilder sb = new StringBuilder();
        if (!stylesDeclared.isEmpty()) {
            sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: ");
            for (String name : stylesDeclared) {
                sb.append(guessXmlName(name));

                if (!name.equals(stylesDeclared.last())) {
                    sb.append(", ");    //$NON-NLS-1$
                }
            }

            AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
            AdtPlugin.printToConsole((String)null, sb);
            sb.setLength(0);
        }

        if (!elementsDeclared.isEmpty()) {
            sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: ");
            for (String name : elementsDeclared) {
                sb.append(name);
                if (!name.equals(elementsDeclared.last())) {
                    sb.append(", ");    //$NON-NLS-1$
                }
            }

            AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
            AdtPlugin.printToConsole((String)null, sb);
        }

        return true;
    }

    /**
     * Performs an approximate translation of the style name into a potential
     * xml name. This is more or less the reverse from guessStyleName().
     *
     * @return The XML local name for a given style name.
     */
    private String guessXmlName(String name) {
        StringBuilder sb = new StringBuilder();
        if (ANDROID_MANIFEST_STYLEABLE.equals(name)) {
            sb.append(MANIFEST_NODE_NAME);
        } else {
            name = name.replace(ANDROID_MANIFEST_STYLEABLE, "");                //$NON-NLS-1$
            boolean first_char = true;
            for (char c : name.toCharArray()) {
                if (c >= 'A' && c <= 'Z') {
                    if (!first_char) {
                        sb.append('-');
                    }
                    c = (char) (c - 'A' + 'a');
                }
                sb.append(c);
                first_char = false;
            }
        }
        return sb.toString();
    }

    /**
     * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the
     * {@link ElementDescriptor} names defined by the tree of descriptors.
     * <p/>
     * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s.
     */
    private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) {
        declared.add(element.getXmlName());
        for (ElementDescriptor desc : element.getChildren()) {
            findAllElementNames(desc, declared);
        }
    }


}
