/*
 * 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.resources.platform.ViewClassInfo;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.IAndroidTarget;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import java.util.HashMap;
import java.util.List;

/**
 * Service responsible for creating/managing {@link ViewElementDescriptor} objects for custom
 * View classes per project.
 * <p/>
 * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring
 * starts once a request for an {@link ViewElementDescriptor} object has been done for a specific
 * class.
 * <p/>
 * The monitoring will notify a listener of any changes in the class triggering a change in its
 * associated {@link ViewElementDescriptor} object.
 * <p/>
 * If the custom class does not exist, no monitoring is put in place to avoid having to listen
 * to all class changes in the projects.
 */
public final class CustomViewDescriptorService {

    private static CustomViewDescriptorService sThis = new CustomViewDescriptorService();

    /**
     * Map where keys are the project, and values are another map containing all the known
     * custom View class for this project. The custom View class are stored in a map
     * where the keys are the fully qualified class name, and the values are their associated
     * {@link ViewElementDescriptor}.
     */
    private HashMap<IProject, HashMap<String, ViewElementDescriptor>> mCustomDescriptorMap =
        new HashMap<IProject, HashMap<String, ViewElementDescriptor>>();

    /**
     * TODO will be used to update the ViewElementDescriptor of the custom view when it
     * is modified (either the class itself or its attributes.xml)
     */
    @SuppressWarnings("unused")
    private ICustomViewDescriptorListener mListener;

    /**
     * Classes which implements this interface provide a method that deal with modifications
     * in custom View class triggering a change in its associated {@link ViewClassInfo} object.
     */
    public interface ICustomViewDescriptorListener {
        /**
         * Sent when a custom View class has changed and
         * its {@link ViewElementDescriptor} was modified.
         *
         * @param project the project containing the class.
         * @param className the fully qualified class name.
         * @param descriptor the updated ElementDescriptor.
         */
        public void updatedClassInfo(IProject project,
                                     String className,
                                     ViewElementDescriptor descriptor);
    }

    /**
     * Returns the singleton instance of {@link CustomViewDescriptorService}.
     */
    public static CustomViewDescriptorService getInstance() {
        return sThis;
    }

    /**
     * Sets the listener receiving custom View class modification notifications.
     * @param listener the listener to receive the notifications.
     *
     * TODO will be used to update the ViewElementDescriptor of the custom view when it
     * is modified (either the class itself or its attributes.xml)
     */
    public void setListener(ICustomViewDescriptorListener listener) {
        mListener = listener;
    }

    /**
     * Returns the {@link ViewElementDescriptor} for a particular project/class when the
     * fully qualified class name actually matches a class from the given project.
     * <p/>
     * Custom descriptors are created as needed.
     * <p/>
     * If it is the first time the {@link ViewElementDescriptor} is requested, the method
     * will check that the specified class is in fact a custom View class. Once this is
     * established, a monitoring for that particular class is initiated. Any change will
     * trigger a notification to the {@link ICustomViewDescriptorListener}.
     *
     * @param project the project containing the class.
     * @param fqcn the fully qualified name of the class.
     * @return a {@link ViewElementDescriptor} or <code>null</code> if the class was not
     *         a custom View class.
     */
    public ViewElementDescriptor getDescriptor(IProject project, String fqcn) {
        // look in the map first
        synchronized (mCustomDescriptorMap) {
            HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);

            if (map != null) {
                ViewElementDescriptor descriptor = map.get(fqcn);
                if (descriptor != null) {
                    return descriptor;
                }
            }

            // if we step here, it looks like we haven't created it yet.
            // First lets check this is in fact a valid type in the project

            try {
                // We expect the project to be both opened and of java type (since it's an android
                // project), so we can create a IJavaProject object from our IProject.
                IJavaProject javaProject = JavaCore.create(project);

                // replace $ by . in the class name
                String javaClassName = fqcn.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$

                // look for the IType object for this class
                IType type = javaProject.findType(javaClassName);
                if (type != null && type.exists()) {
                    // the type exists. Let's get the parent class and its ViewClassInfo.

                    // get the type hierarchy
                    ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
                            new NullProgressMonitor());

                    ViewElementDescriptor parentDescriptor = createViewDescriptor(
                            hierarchy.getSuperclass(type), project, hierarchy);

                    if (parentDescriptor != null) {
                        // we have a valid parent, lets create a new ViewElementDescriptor.

                        ViewElementDescriptor descriptor = new ViewElementDescriptor(fqcn,
                                fqcn, // ui_name
                                fqcn, // canonical class name
                                null, // tooltip
                                null, // sdk_url
                                getAttributeDescriptor(type, parentDescriptor),
                                null, // layout attributes
                                null, // children
                                false /* mandatory */);

                        descriptor.setSuperClass(parentDescriptor);

                        synchronized (mCustomDescriptorMap) {
                            map = mCustomDescriptorMap.get(project);
                            if (map == null) {
                                map = new HashMap<String, ViewElementDescriptor>();
                                mCustomDescriptorMap.put(project, map);
                            }

                            map.put(fqcn, descriptor);
                        }

                        //TODO setup listener on this resource change.

                        return descriptor;
                    }
                }
            } catch (JavaModelException e) {
                // there was an error accessing any of the IType, we'll just return null;
            }
        }

        return null;
    }

    /**
     * Computes (if needed) and returns the {@link ViewElementDescriptor} for the specified type.
     *
     * @return A {@link ViewElementDescriptor} or null if type or typeHierarchy is null.
     */
    private ViewElementDescriptor createViewDescriptor(IType type, IProject project,
            ITypeHierarchy typeHierarchy) {
        // check if the type is a built-in View class.
        List<ViewElementDescriptor> builtInList = null;

        Sdk currentSdk = Sdk.getCurrent();
        if (currentSdk != null) {
            IAndroidTarget target = currentSdk.getTarget(project);
            if (target != null) {
                AndroidTargetData data = currentSdk.getTargetData(target);
                if (data != null) {
                    builtInList = data.getLayoutDescriptors().getViewDescriptors();
                }
            }
        }

        // give up if there's no type
        if (type == null) {
            return null;
        }

        String fqcn = type.getFullyQualifiedName();

        if (builtInList != null) {
            for (ViewElementDescriptor viewDescriptor : builtInList) {
                if (fqcn.equals(viewDescriptor.getFullClassName())) {
                    return viewDescriptor;
                }
            }
        }

        // it's not a built-in class? Lets look if the superclass is built-in
        // give up if there's no type
        if (typeHierarchy == null) {
            return null;
        }

        IType parentType = typeHierarchy.getSuperclass(type);
        if (parentType != null) {
            ViewElementDescriptor parentDescriptor = createViewDescriptor(parentType, project,
                    typeHierarchy);

            if (parentDescriptor != null) {
                // parent class is a valid View class with a descriptor, so we create one
                // for this class.
                ViewElementDescriptor descriptor = new ViewElementDescriptor(fqcn,
                        fqcn, // ui_name
                        fqcn, // canonical name
                        null, // tooltip
                        null, // sdk_url
                        getAttributeDescriptor(type, parentDescriptor),
                        null, // layout attributes
                        null, // children
                        false /* mandatory */);

                descriptor.setSuperClass(parentDescriptor);

                // add it to the map
                synchronized (mCustomDescriptorMap) {
                    HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);

                    if (map == null) {
                        map = new HashMap<String, ViewElementDescriptor>();
                        mCustomDescriptorMap.put(project, map);
                    }

                    map.put(fqcn, descriptor);

                }

                //TODO setup listener on this resource change.

                return descriptor;
            }
        }

        // class is neither a built-in view class, nor extend one. return null.
        return null;
    }

    /**
     * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}.
     * <p/>
     * The array should contain the descriptor for this type and all its supertypes.
     *
     * @param type the type for which the {@link AttributeDescriptor} are returned.
     * @param parentDescriptor the {@link ViewElementDescriptor} of the direct superclass.
     */
    private AttributeDescriptor[] getAttributeDescriptor(IType type,
            ViewElementDescriptor parentDescriptor) {
        // TODO add the class attribute descriptors to the parent descriptors.
        return parentDescriptor.getAttributes();
    }
}
