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

import com.android.ide.common.api.IAttributeInfo.Format;
import com.android.ide.common.resources.platform.AttributeInfo;
import com.android.ide.common.resources.platform.ViewClassInfo;
import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
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.DocumentDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider;
import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttributeDescriptor;
import com.android.sdklib.SdkConstants;

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


/**
 * Complete description of the layout structure.
 */
public final class LayoutDescriptors implements IDescriptorProvider {

    /**
     * The XML name of the special &lt;include&gt; layout tag.
     * A synthetic element with that name is created as part of the view descriptors list
     * returned by {@link #getViewDescriptors()}.
     */
    public static final String VIEW_INCLUDE = "include";      //$NON-NLS-1$

    /**
     * The XML name of the special &lt;merge&gt; layout tag.
     * A synthetic element with that name is created as part of the view descriptors list
     * returned by {@link #getViewDescriptors()}.
     */
    public static final String VIEW_MERGE = "merge";          //$NON-NLS-1$

    /**
     * The attribute name of the include tag's url naming the resource to be inserted
     * <p>
     * <b>NOTE</b>: The layout attribute is NOT in the Android namespace!
     */
    public static final String ATTR_LAYOUT = "layout"; //$NON-NLS-1$

    // Public attributes names, attributes descriptors and elements descriptors
    public static final String ID_ATTR = "id"; //$NON-NLS-1$

    /** The document descriptor. Contains all layouts and views linked together. */
    private DocumentDescriptor mRootDescriptor =
        new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$

    /** The list of all known ViewLayout descriptors. */
    private List<ViewElementDescriptor> mLayoutDescriptors =
        new ArrayList<ViewElementDescriptor>();

    /** Read-Only list of View Descriptors. */
    private List<ViewElementDescriptor> mROLayoutDescriptors;

    /** The list of all known View (not ViewLayout) descriptors. */
    private List<ViewElementDescriptor> mViewDescriptors = new ArrayList<ViewElementDescriptor>();

    /** Read-Only list of View Descriptors. */
    private List<ViewElementDescriptor> mROViewDescriptors;

    /** The descriptor matching android.view.View. */
    private ViewElementDescriptor mBaseViewDescriptor;

    /** Returns the document descriptor. Contains all layouts and views linked together. */
    public DocumentDescriptor getDescriptor() {
        return mRootDescriptor;
    }

    /** Returns the read-only list of all known ViewLayout descriptors. */
    public List<ViewElementDescriptor> getLayoutDescriptors() {
        return mROLayoutDescriptors;
    }

    /** Returns the read-only list of all known View (not ViewLayout) descriptors. */
    public List<ViewElementDescriptor> getViewDescriptors() {
        return mROViewDescriptors;
    }

    public ElementDescriptor[] getRootElementDescriptors() {
        return mRootDescriptor.getChildren();
    }

    /**
     * Returns the descriptor matching android.view.View, which is guaranteed
     * to be a {@link ViewElementDescriptor}.
     */
    public ViewElementDescriptor getBaseViewDescriptor() {
        if (mBaseViewDescriptor == null) {
            for (ElementDescriptor desc : mViewDescriptors) {
                if (desc instanceof ViewElementDescriptor) {
                    ViewElementDescriptor viewDesc = (ViewElementDescriptor) desc;
                    if (SdkConstants.CLASS_VIEW.equals(viewDesc.getFullClassName())) {
                        mBaseViewDescriptor = viewDesc;
                        break;
                    }
                }

            }
        }
        return mBaseViewDescriptor;
    }

    /**
     * Updates the document descriptor.
     * <p/>
     * It first computes the new children of the descriptor and then update them
     * all at once.
     * <p/>
     *  TODO: differentiate groups from views in the tree UI? => rely on icons
     * <p/>
     *
     * @param views The list of views in the framework.
     * @param layouts The list of layouts in the framework.
     */
    public synchronized void updateDescriptors(ViewClassInfo[] views, ViewClassInfo[] layouts) {

        // This map links every ViewClassInfo to the ElementDescriptor we created.
        // It is filled by convertView() and used later to fix the super-class hierarchy.
        HashMap<ViewClassInfo, ViewElementDescriptor> infoDescMap =
            new HashMap<ViewClassInfo, ViewElementDescriptor>();

        ArrayList<ViewElementDescriptor> newViews = new ArrayList<ViewElementDescriptor>();
        if (views != null) {
            for (ViewClassInfo info : views) {
                ViewElementDescriptor desc = convertView(info, infoDescMap);
                newViews.add(desc);
            }
        }

        // Create <include> as a synthetic regular view.
        // Note: ViewStub is already described by attrs.xml
        insertInclude(newViews);

        List<ViewElementDescriptor> newLayouts = new ArrayList<ViewElementDescriptor>();
        if (layouts != null) {
            for (ViewClassInfo info : layouts) {
                ViewElementDescriptor desc = convertView(info, infoDescMap);
                newLayouts.add(desc);
            }
        }

        List<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>();
        newDescriptors.addAll(newLayouts);
        newDescriptors.addAll(newViews);

        // Link all layouts to everything else here.. recursively
        for (ViewElementDescriptor layoutDesc : newLayouts) {
            layoutDesc.setChildren(newDescriptors);
        }

        fixSuperClasses(infoDescMap);

        // The <merge> tag can only be a root tag, so it is added at the end.
        // It gets everything else as children but it is not made a child itself.
        ViewElementDescriptor mergeTag = createMerge(newLayouts);
        mergeTag.setChildren(newDescriptors);  // mergeTag makes a copy of the list
        newDescriptors.add(mergeTag);
        newLayouts.add(mergeTag);

        // Sort palette contents
        Collections.sort(newViews);
        Collections.sort(newLayouts);

        mViewDescriptors = newViews;
        mLayoutDescriptors  = newLayouts;
        mRootDescriptor.setChildren(newDescriptors);

        mBaseViewDescriptor = null;
        mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors);
        mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors);
    }

    /**
     * Creates an element descriptor from a given {@link ViewClassInfo}.
     *
     * @param info The {@link ViewClassInfo} to convert into a new {@link ViewElementDescriptor}.
     * @param infoDescMap This map links every ViewClassInfo to the ElementDescriptor it created.
     *                    It is filled by here and used later to fix the super-class hierarchy.
     */
    private ViewElementDescriptor convertView(
            ViewClassInfo info,
            HashMap<ViewClassInfo, ViewElementDescriptor> infoDescMap) {
        String xml_name = info.getShortClassName();
        String tooltip = info.getJavaDoc();

        ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();

        // All views and groups have an implicit "style" attribute which is a reference.
        AttributeInfo styleInfo = new AttributeInfo(
                "style",    //$NON-NLS-1$ xmlLocalName
                new Format[] { Format.REFERENCE });
        styleInfo.setJavaDoc("A reference to a custom style"); //tooltip
        DescriptorsUtils.appendAttribute(attributes,
                "style",    //$NON-NLS-1$
                null,       //nsUri
                styleInfo,
                false,      //required
                null);      // overrides

        // Process all View attributes
        DescriptorsUtils.appendAttributes(attributes,
                null, // elementName
                SdkConstants.NS_RESOURCES,
                info.getAttributes(),
                null, // requiredAttributes
                null /* overrides */);

        for (ViewClassInfo link = info.getSuperClass();
                link != null;
                link = link.getSuperClass()) {
            AttributeInfo[] attrList = link.getAttributes();
            if (attrList.length > 0) {
                attributes.add(new SeparatorAttributeDescriptor(
                        String.format("Attributes from %1$s", link.getShortClassName())));
                DescriptorsUtils.appendAttributes(attributes,
                        null, // elementName
                        SdkConstants.NS_RESOURCES,
                        attrList,
                        null, // requiredAttributes
                        null /* overrides */);
            }
        }

        // Process all LayoutParams attributes
        ArrayList<AttributeDescriptor> layoutAttributes = new ArrayList<AttributeDescriptor>();
        LayoutParamsInfo layoutParams = info.getLayoutData();

        for(; layoutParams != null; layoutParams = layoutParams.getSuperClass()) {
            boolean need_separator = true;
            for (AttributeInfo attr_info : layoutParams.getAttributes()) {
                if (DescriptorsUtils.containsAttribute(layoutAttributes,
                        SdkConstants.NS_RESOURCES, attr_info)) {
                    continue;
                }
                if (need_separator) {
                    String title;
                    if (layoutParams.getShortClassName().equals(
                            SdkConstants.CLASS_NAME_LAYOUTPARAMS)) {
                        title = String.format("Layout Attributes from %1$s",
                                    layoutParams.getViewLayoutClass().getShortClassName());
                    } else {
                        title = String.format("Layout Attributes from %1$s (%2$s)",
                                layoutParams.getViewLayoutClass().getShortClassName(),
                                layoutParams.getShortClassName());
                    }
                    layoutAttributes.add(new SeparatorAttributeDescriptor(title));
                    need_separator = false;
                }
                DescriptorsUtils.appendAttribute(layoutAttributes,
                        null, // elementName
                        SdkConstants.NS_RESOURCES,
                        attr_info,
                        false, // required
                        null /* overrides */);
            }
        }

        ViewElementDescriptor desc = new ViewElementDescriptor(xml_name,
                xml_name, // ui_name
                info.getFullClassName(),
                tooltip,
                null, // sdk_url
                attributes.toArray(new AttributeDescriptor[attributes.size()]),
                layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]),
                null, // children
                false /* mandatory */);
        infoDescMap.put(info, desc);
        return desc;
    }

    /**
     * Creates a new <include> descriptor and adds it to the list of view descriptors.
     *
     * @param knownViews A list of view descriptors being populated. Also used to find the
     *   View descriptor and extract its layout attributes.
     */
    private void insertInclude(List<ViewElementDescriptor> knownViews) {
        String xml_name = VIEW_INCLUDE;

        // Create the include custom attributes
        ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();

        // Note that the "layout" attribute does NOT have the Android namespace
        DescriptorsUtils.appendAttribute(attributes,
                null, //elementXmlName
                null, //nsUri
                new AttributeInfo(
                        ATTR_LAYOUT,
                        new Format[] { Format.REFERENCE } ),
                true,  //required
                null); //overrides

        DescriptorsUtils.appendAttribute(attributes,
                null, //elementXmlName
                SdkConstants.NS_RESOURCES, //nsUri
                new AttributeInfo(
                        "id",           //$NON-NLS-1$
                        new Format[] { Format.REFERENCE } ),
                true,  //required
                null); //overrides

        // Find View and inherit all its layout attributes
        AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
                SdkConstants.CLASS_VIEW, knownViews);

        // Create the include descriptor
        ViewElementDescriptor desc = new ViewElementDescriptor(xml_name,  // xml_name
                xml_name, // ui_name
                VIEW_INCLUDE, // "class name"; the GLE only treats this as an element tag
                "Lets you statically include XML layouts inside other XML layouts.",  // tooltip
                null, // sdk_url
                attributes.toArray(new AttributeDescriptor[attributes.size()]),
                viewLayoutAttribs,  // layout attributes
                null, // children
                false /* mandatory */);

        knownViews.add(desc);
    }

    /**
     * Creates and return a new <merge> descriptor.
     * @param knownLayouts  A list of all known layout view descriptors, used to find the
     *   FrameLayout descriptor and extract its layout attributes.
     */
    private ViewElementDescriptor createMerge(List<ViewElementDescriptor> knownLayouts) {
        String xml_name = VIEW_MERGE;

        // Find View and inherit all its layout attributes
        AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes(
                SdkConstants.CLASS_FRAMELAYOUT, knownLayouts);

        // Create the include descriptor
        ViewElementDescriptor desc = new ViewElementDescriptor(xml_name,  // xml_name
                xml_name, // ui_name
                VIEW_MERGE, // "class name"; the GLE only treats this as an element tag
                "A root tag useful for XML layouts inflated using a ViewStub.",  // tooltip
                null,  // sdk_url
                null,  // attributes
                viewLayoutAttribs,  // layout attributes
                null,  // children
                false  /* mandatory */);

        return desc;
    }

    /**
     * Finds the descriptor and retrieves all its layout attributes.
     */
    private AttributeDescriptor[] findViewLayoutAttributes(
            String viewFqcn,
            List<ViewElementDescriptor> knownViews) {

        for (ViewElementDescriptor viewDesc : knownViews) {
            if (viewFqcn.equals(viewDesc.getFullClassName())) {
                return viewDesc.getLayoutAttributes();
            }
        }

        return null;
    }

    /**
     * Set the super-class of each {@link ViewElementDescriptor} by using the super-class
     * information available in the {@link ViewClassInfo}.
     */
    private void fixSuperClasses(Map<ViewClassInfo, ViewElementDescriptor> infoDescMap) {

        for (Entry<ViewClassInfo, ViewElementDescriptor> entry : infoDescMap.entrySet()) {
            ViewClassInfo info = entry.getKey();
            ViewElementDescriptor desc = entry.getValue();

            ViewClassInfo sup = info.getSuperClass();
            if (sup != null) {
                ViewElementDescriptor supDesc = infoDescMap.get(sup);
                while (supDesc == null && sup != null) {
                    // We don't have a descriptor for the super-class. That means the class is
                    // probably abstract, so we just need to walk up the super-class chain till
                    // we find one we have. All views derive from android.view.View so we should
                    // surely find that eventually.
                    sup = sup.getSuperClass();
                    if (sup != null) {
                        supDesc = infoDescMap.get(sup);
                    }
                }
                if (supDesc != null) {
                    desc.setSuperClass(supDesc);
                }
            }
        }
    }
}
