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

import com.android.ide.common.resources.platform.AttrsXmlParser;
import com.android.ide.common.resources.platform.ViewClassInfo;
import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.sdk.IAndroidClassLoader.IClassDescriptor;
import com.android.sdklib.SdkConstants;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.management.InvalidAttributeValueException;

/*
 * TODO: refactor this. Could use some cleanup.
 */

/**
 * Parser for the framework library.
 * <p/>
 * This gather the following information:
 * <ul>
 * <li>Resource ID from <code>android.R</code></li>
 * <li>The list of permissions values from <code>android.Manifest$permission</code></li>
 * <li></li>
 * </ul>
 */
public class LayoutParamsParser {

    /**
     * Class extending {@link ViewClassInfo} by adding the notion of instantiability.
     * {@link LayoutParamsParser#getViews()} and {@link LayoutParamsParser#getGroups()} should
     * only return classes that can be instantiated.
     */
    final static class ExtViewClassInfo extends ViewClassInfo {

        private boolean mIsInstantiable;

        ExtViewClassInfo(boolean instantiable, boolean isLayout, String canonicalClassName,
                String shortClassName) {
            super(isLayout, canonicalClassName, shortClassName);
            mIsInstantiable = instantiable;
        }

        boolean isInstantiable() {
            return mIsInstantiable;
        }
    }

    /* Note: protected members/methods are overridden in unit tests */

    /** Reference to android.view.View */
    protected IClassDescriptor mTopViewClass;
    /** Reference to android.view.ViewGroup */
    protected IClassDescriptor mTopGroupClass;
    /** Reference to android.view.ViewGroup$LayoutParams */
    protected IClassDescriptor mTopLayoutParamsClass;

    /** Input list of all classes deriving from android.view.View */
    protected ArrayList<IClassDescriptor> mViewList;
    /** Input list of all classes deriving from android.view.ViewGroup */
    protected ArrayList<IClassDescriptor> mGroupList;

    /** Output map of FQCN => info on View classes */
    protected TreeMap<String, ExtViewClassInfo> mViewMap;
    /** Output map of FQCN => info on ViewGroup classes */
    protected TreeMap<String, ExtViewClassInfo> mGroupMap;
    /** Output map of FQCN => info on LayoutParams classes */
    protected HashMap<String, LayoutParamsInfo> mLayoutParamsMap;

    /** The attrs.xml parser */
    protected AttrsXmlParser mAttrsXmlParser;

    /** The android.jar class loader */
    protected IAndroidClassLoader mClassLoader;

    /**
     * Instantiate a new LayoutParamsParser.
     * @param classLoader The android.jar class loader
     * @param attrsXmlParser The parser of the attrs.xml file
     */
    public LayoutParamsParser(IAndroidClassLoader classLoader,
            AttrsXmlParser attrsXmlParser) {
        mClassLoader = classLoader;
        mAttrsXmlParser = attrsXmlParser;
    }

    /** Returns the map of FQCN => info on View classes */
    public List<ViewClassInfo> getViews() {
        return getInstantiables(mViewMap);
    }

    /** Returns the map of FQCN => info on ViewGroup classes */
    public List<ViewClassInfo> getGroups() {
        return getInstantiables(mGroupMap);
    }

    /**
     * TODO: doc here.
     * <p/>
     * Note: on output we should have NO dependency on {@link IClassDescriptor},
     * otherwise we wouldn't be able to unload the class loader later.
     * <p/>
     * Note on Vocabulary: FQCN=Fully Qualified Class Name (e.g. "my.package.class$innerClass")
     * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
     */
    public void parseLayoutClasses(IProgressMonitor monitor) {
        parseClasses(monitor,
                SdkConstants.CLASS_VIEW,
                SdkConstants.CLASS_VIEWGROUP,
                SdkConstants.CLASS_VIEWGROUP_LAYOUTPARAMS);
    }

    public void parsePreferencesClasses(IProgressMonitor monitor) {
        parseClasses(monitor,
                SdkConstants.CLASS_PREFERENCE,
                SdkConstants.CLASS_PREFERENCEGROUP,
                null /* paramsClassName */ );
    }

    private void parseClasses(IProgressMonitor monitor,
            String rootClassName,
            String groupClassName,
            String paramsClassName) {
        try {
            SubMonitor progress = SubMonitor.convert(monitor, 100);

            String[] superClasses = new String[2 + (paramsClassName == null ? 0 : 1)];
            superClasses[0] = groupClassName;
            superClasses[1] = rootClassName;
            if (paramsClassName != null) {
                superClasses[2] = paramsClassName;
            }
            HashMap<String, ArrayList<IClassDescriptor>> found =
                    mClassLoader.findClassesDerivingFrom("android.", superClasses);  //$NON-NLS-1$
            mTopViewClass = mClassLoader.getClass(rootClassName);
            mTopGroupClass = mClassLoader.getClass(groupClassName);
            if (paramsClassName != null) {
                mTopLayoutParamsClass = mClassLoader.getClass(paramsClassName);
            }

            mViewList = found.get(rootClassName);
            mGroupList = found.get(groupClassName);

            mViewMap = new TreeMap<String, ExtViewClassInfo>();
            mGroupMap = new TreeMap<String, ExtViewClassInfo>();
            if (mTopLayoutParamsClass != null) {
                mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>();
            }

            // Add top classes to the maps since by design they are not listed in classes deriving
            // from themselves.
            if (mTopGroupClass != null) {
                addGroup(mTopGroupClass);
            }
            if (mTopViewClass != null) {
                addView(mTopViewClass);
            }

            // ViewGroup derives from View
            ExtViewClassInfo vg = mGroupMap.get(groupClassName);
            if (vg != null) {
                vg.setSuperClass(mViewMap.get(rootClassName));
            }

            progress.setWorkRemaining(mGroupList.size() + mViewList.size());

            for (IClassDescriptor groupChild : mGroupList) {
                addGroup(groupChild);
                progress.worked(1);
            }

            for (IClassDescriptor viewChild : mViewList) {
                if (viewChild != mTopGroupClass) {
                    addView(viewChild);
                }
                progress.worked(1);
            }
        } catch (ClassNotFoundException e) {
            AdtPlugin.log(e, "Problem loading class %1$s or %2$s",  //$NON-NLS-1$
                    rootClassName, groupClassName);
        } catch (InvalidAttributeValueException e) {
            AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
        } catch (ClassFormatError e) {
            AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
        } catch (IOException e) {
            AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
        }
    }

    /**
     * Parses a View class and adds a ExtViewClassInfo for it in mViewMap.
     * It calls itself recursively to handle super classes which are also Views.
     */
    private ExtViewClassInfo addView(IClassDescriptor viewClass) {
        String fqcn = viewClass.getFullClassName();
        if (mViewMap.containsKey(fqcn)) {
            return mViewMap.get(fqcn);
        } else if (mGroupMap.containsKey(fqcn)) {
            return mGroupMap.get(fqcn);
        }

        ExtViewClassInfo info = new ExtViewClassInfo(viewClass.isInstantiable(),
                false /* layout */, fqcn, viewClass.getSimpleName());
        mViewMap.put(fqcn, info);

        // All view classes derive from mTopViewClass by design.
        // Do not lookup the super class for mTopViewClass itself.
        if (viewClass.equals(mTopViewClass) == false) {
            IClassDescriptor superClass = viewClass.getSuperclass();
            ExtViewClassInfo superClassInfo = addView(superClass);
            info.setSuperClass(superClassInfo);
        }

        mAttrsXmlParser.loadViewAttributes(info);
        return info;
    }

    /**
     * Parses a ViewGroup class and adds a ExtViewClassInfo for it in mGroupMap.
     * It calls itself recursively to handle super classes which are also ViewGroups.
     */
    private ExtViewClassInfo addGroup(IClassDescriptor groupClass) {
        String fqcn = groupClass.getFullClassName();
        if (mGroupMap.containsKey(fqcn)) {
            return mGroupMap.get(fqcn);
        }

        ExtViewClassInfo info = new ExtViewClassInfo(groupClass.isInstantiable(),
                true /* layout */, fqcn, groupClass.getSimpleName());
        mGroupMap.put(fqcn, info);

        // All groups derive from android.view.ViewGroup, which in turns derives from
        // android.view.View (i.e. mTopViewClass here). So the only group that can have View as
        // its super class is the ViewGroup base class and we don't try to resolve it since groups
        // are loaded before views.
        IClassDescriptor superClass = groupClass.getSuperclass();

        // Assertion: at this point, we should have
        //   superClass != mTopViewClass || fqcn.equals(AndroidConstants.CLASS_VIEWGROUP);

        if (superClass != null && superClass.equals(mTopViewClass) == false) {
            ExtViewClassInfo superClassInfo = addGroup(superClass);

            // Assertion: we should have superClassInfo != null && superClassInfo != info;
            if (superClassInfo != null && superClassInfo != info) {
                info.setSuperClass(superClassInfo);
            }
        }

        mAttrsXmlParser.loadViewAttributes(info);
        if (mTopLayoutParamsClass != null) {
            info.setLayoutParams(addLayoutParams(groupClass));
        }
        return info;
    }

    /**
     * Parses a ViewGroup class and returns an info object on its inner LayoutParams.
     *
     * @return The {@link LayoutParamsInfo} for the ViewGroup class or null.
     */
    private LayoutParamsInfo addLayoutParams(IClassDescriptor groupClass) {

        // Is there a LayoutParams in this group class?
        IClassDescriptor layoutParamsClass = findLayoutParams(groupClass);

        // if there's no layout data in the group class, link to the one from the
        // super class.
        if (layoutParamsClass == null) {
            for (IClassDescriptor superClass = groupClass.getSuperclass();
                    layoutParamsClass == null &&
                        superClass != null &&
                        superClass.equals(mTopViewClass) == false;
                    superClass = superClass.getSuperclass()) {
                layoutParamsClass = findLayoutParams(superClass);
            }
        }

        if (layoutParamsClass != null) {
            return getLayoutParamsInfo(layoutParamsClass);
        }

        return null;
    }

    /**
     * Parses a LayoutParams class and returns a LayoutParamsInfo object for it.
     * It calls itself recursively to handle the super class of the LayoutParams.
     */
    private LayoutParamsInfo getLayoutParamsInfo(IClassDescriptor layoutParamsClass) {
        String fqcn = layoutParamsClass.getFullClassName();
        LayoutParamsInfo layoutParamsInfo = mLayoutParamsMap.get(fqcn);

        if (layoutParamsInfo != null) {
            return layoutParamsInfo;
        }

        // Find the link on the LayoutParams super class
        LayoutParamsInfo superClassInfo = null;
        if (layoutParamsClass.equals(mTopLayoutParamsClass) == false) {
            IClassDescriptor superClass = layoutParamsClass.getSuperclass();
            superClassInfo = getLayoutParamsInfo(superClass);
        }

        // Find the link on the enclosing ViewGroup
        ExtViewClassInfo enclosingGroupInfo = addGroup(layoutParamsClass.getEnclosingClass());

        layoutParamsInfo = new ExtViewClassInfo.LayoutParamsInfo(
                enclosingGroupInfo, layoutParamsClass.getSimpleName(), superClassInfo);
        mLayoutParamsMap.put(fqcn, layoutParamsInfo);

        mAttrsXmlParser.loadLayoutParamsAttributes(layoutParamsInfo);

        return layoutParamsInfo;
    }

    /**
     * Given a ViewGroup-derived class, looks for an inner class named LayoutParams
     * and if found returns its class definition.
     * <p/>
     * This uses the actual defined inner classes and does not look at inherited classes.
     *
     * @param groupClass The ViewGroup derived class
     * @return The Class of the inner LayoutParams or null if none is declared.
     */
    private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) {
        IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses();
        for (IClassDescriptor innerClass : innerClasses) {
            if (innerClass.getSimpleName().equals(SdkConstants.CLASS_NAME_LAYOUTPARAMS)) {
                return innerClass;
            }
        }
        return null;
    }

    /**
     * Computes and return a list of ViewClassInfo from a map by filtering out the class that
     * cannot be instantiated.
     */
    private List<ViewClassInfo> getInstantiables(SortedMap<String, ExtViewClassInfo> map) {
        Collection<ExtViewClassInfo> values = map.values();
        ArrayList<ViewClassInfo> list = new ArrayList<ViewClassInfo>();

        for (ExtViewClassInfo info : values) {
            if (info.isInstantiable()) {
                list.add(info);
            }
        }

        return list;
    }
}
