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

import static com.android.sdklib.SdkConstants.CLASS_VIEW;
import static com.android.sdklib.SdkConstants.CLASS_VIEWGROUP;
import static com.android.sdklib.SdkConstants.FN_FRAMEWORK_LIBRARY;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.util.Pair;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.core.ResolvedBinaryType;
import org.eclipse.jdt.internal.core.ResolvedSourceType;
import org.eclipse.swt.widgets.Display;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * The {@link CustomViewFinder} can look up the custom views and third party views
 * available for a given project.
 */
@SuppressWarnings("restriction") // JDT model access for custom-view class lookup
public class CustomViewFinder {
    /**
     * Qualified name for the per-project non-persistent property storing the
     * {@link CustomViewFinder} for this project
     */
    private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
            "viewfinder"); //$NON-NLS-1$

    /** Project that this view finder locates views for */
    private final IProject mProject;

    private final List<Listener> mListeners = new ArrayList<Listener>();

    private List<String> mCustomViews;
    private List<String> mThirdPartyViews;
    private boolean mRefreshing;

    /**
     * Constructs an {@link CustomViewFinder} for the given project. Don't use this method;
     * use the {@link #get} factory method instead.
     *
     * @param project project to create an {@link CustomViewFinder} for
     */
    private CustomViewFinder(IProject project) {
        mProject = project;
    }

    /**
     * Returns the {@link CustomViewFinder} for the given project
     *
     * @param project the project the finder is associated with
     * @return a {@CustomViewFinder} for the given project, never null
     */
    public static CustomViewFinder get(IProject project) {
        CustomViewFinder finder = null;
        try {
            finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER);
        } catch (CoreException e) {
            // Not a problem; we will just create a new one
        }

        if (finder == null) {
            finder = new CustomViewFinder(project);
            try {
                project.setSessionProperty(CUSTOM_VIEW_FINDER, finder);
            } catch (CoreException e) {
                AdtPlugin.log(e, "Can't store CustomViewFinder");
            }
        }

        return finder;
    }

    public void refresh() {
        refresh(null /*listener*/, true /* sync */);
    }

    public void refresh(final Listener listener) {
        refresh(listener, false /* sync */);
    }

    private void refresh(final Listener listener, boolean sync) {
        // Add this listener to the list of listeners which should be notified when the
        // search is done. (There could be more than one since multiple requests could
        // arrive for a slow search since the search is run in a different thread).
        if (listener != null) {
            synchronized (this) {
                mListeners.add(listener);
            }
        }
        synchronized (this) {
            if (listener != null) {
                mListeners.add(listener);
            }
            if (mRefreshing) {
                return;
            }
            mRefreshing = true;
        }

        FindViewsJob job = new FindViewsJob();
        job.schedule();
        if (sync) {
            try {
                job.join();
            } catch (InterruptedException e) {
                AdtPlugin.log(e, null);
            }
        }
    }

    public Collection<String> getCustomViews() {
        return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
    }

    public Collection<String> getThirdPartyViews() {
        return mThirdPartyViews == null
            ? null : Collections.unmodifiableCollection(mThirdPartyViews);
    }

    public Collection<String> getAllViews() {
        // Not yet initialized: return null
        if (mCustomViews == null) {
            return null;
        }
        List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size());
        all.addAll(mCustomViews);
        all.addAll(mThirdPartyViews);
        return all;
    }

    /**
     * Returns a pair of view lists - the custom views and the 3rd-party views.
     * This method performs no caching; it is the same as asking the custom view finder
     * to refresh itself and then waiting for the answer and returning it.
     *
     * @param project the Android project
     * @param layoutsOnly if true, only search for layouts
     * @return a pair of lists, the first containing custom views and the second
     *         containing 3rd party views
     */
    public static Pair<List<String>,List<String>> findViews(
            final IProject project, boolean layoutsOnly) {
        CustomViewFinder finder = get(project);

        return finder.findViews(layoutsOnly);
    }

    private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) {
        final Set<String> customViews = new HashSet<String>();
        final Set<String> thirdPartyViews = new HashSet<String>();

        ProjectState state = Sdk.getProjectState(mProject);
        final List<IProject> libraries = state != null
            ? state.getFullLibraryProjects() : Collections.<IProject>emptyList();

        SearchRequestor requestor = new SearchRequestor() {
            @Override
            public void acceptSearchMatch(SearchMatch match) throws CoreException {
                // Ignore matches in comments
                if (match.isInsideDocComment()) {
                    return;
                }

                Object element = match.getElement();
                if (element instanceof ResolvedBinaryType) {
                    // Third party view
                    ResolvedBinaryType type = (ResolvedBinaryType) element;
                    IPackageFragment fragment = type.getPackageFragment();
                    IPath path = fragment.getPath();
                    String last = path.lastSegment();
                    // Filter out android.jar stuff
                    if (last.equals(FN_FRAMEWORK_LIBRARY)) {
                        return;
                    }
                    if (!isValidView(type, layoutsOnly)) {
                        return;
                    }

                    IProject matchProject = match.getResource().getProject();
                    if (mProject == matchProject || libraries.contains(matchProject)) {
                        String fqn = type.getFullyQualifiedName();
                        thirdPartyViews.add(fqn);
                    }
                } else if (element instanceof ResolvedSourceType) {
                    // User custom view
                    IProject matchProject = match.getResource().getProject();
                    if (mProject == matchProject || libraries.contains(matchProject)) {
                        ResolvedSourceType type = (ResolvedSourceType) element;
                        if (!isValidView(type, layoutsOnly)) {
                            return;
                        }
                        String fqn = type.getFullyQualifiedName();
                        fqn = fqn.replace('$', '.');
                        customViews.add(fqn);
                    }
                }
            }
        };
        try {
            IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
            if (javaProject != null) {
                String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
                IType viewType = javaProject.findType(className);
                if (viewType != null) {
                    IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType);
                    SearchParticipant[] participants = new SearchParticipant[] {
                        SearchEngine.getDefaultSearchParticipant()
                    };
                    int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;

                    SearchPattern pattern = SearchPattern.createPattern("*",
                            IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS,
                            matchRule);
                    SearchEngine engine = new SearchEngine();
                    engine.search(pattern, participants, scope, requestor,
                            new NullProgressMonitor());
                }
            }
        } catch (CoreException e) {
            AdtPlugin.log(e, null);
        }


        List<String> custom = new ArrayList<String>(customViews);
        List<String> thirdParty = new ArrayList<String>(thirdPartyViews);

        if (!layoutsOnly) {
            // Update our cached answers (unless we were filtered on only layouts)
            mCustomViews = custom;
            mThirdPartyViews = thirdParty;
        }

        return Pair.of(custom, thirdParty);
    }

    /**
     * Determines whether the given member is a valid android.view.View to be added to the
     * list of custom views or third party views. It checks that the view is public and
     * not abstract for example.
     */
    private static boolean isValidView(IType type, boolean layoutsOnly)
            throws JavaModelException {
        // Skip anonymous classes
        if (type.isAnonymous()) {
            return false;
        }
        int flags = type.getFlags();
        if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) {
            return false;
        }

        // TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups
        // not willing to accept children via XML

        // See if the class has one of the acceptable constructors
        // needed for XML instantiation:
        //    View(Context context)
        //    View(Context context, AttributeSet attrs)
        //    View(Context context, AttributeSet attrs, int defStyle)
        // We don't simply do three direct checks via type.getMethod() because the types
        // are not resolved, so we don't know for each parameter if we will get the
        // fully qualified or the unqualified class names.
        // Instead, iterate over the methods and look for a match.
        String typeName = type.getElementName();
        for (IMethod method : type.getMethods()) {
            // Only care about constructors
            if (!method.getElementName().equals(typeName)) {
                continue;
            }

            String[] parameterTypes = method.getParameterTypes();
            if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) {
                continue;
            }

            String first = parameterTypes[0];
            // Look for the parameter type signatures -- produced by
            // JDT's Signature.createTypeSignature("Context", false /*isResolved*/);.
            // This is not a typo; they were copy/pasted from the actual parameter names
            // observed in the debugger examining these data structures.
            if (first.equals("QContext;")                                   //$NON-NLS-1$
                    || first.equals("Qandroid.content.Context;")) {         //$NON-NLS-1$
                if (parameterTypes.length == 1) {
                    return true;
                }
                String second = parameterTypes[1];
                if (second.equals("QAttributeSet;")                         //$NON-NLS-1$
                        || second.equals("Qandroid.util.AttributeSet;")) {  //$NON-NLS-1$
                    if (parameterTypes.length == 2) {
                        return true;
                    }
                    String third = parameterTypes[2];
                    if (third.equals("I")) {                                //$NON-NLS-1$
                        if (parameterTypes.length == 3) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    /**
     * Interface implemented by clients of the {@link CustomViewFinder} to be notified
     * when a custom view search has completed. Will always be called on the SWT event
     * dispatch thread.
     */
    public interface Listener {
        void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews);
    }

    /**
     * Job for performing class search off the UI thread. This is marked as a system job
     * so that it won't show up in the progress monitor etc.
     */
    private class FindViewsJob extends Job {
        FindViewsJob() {
            super("Find Custom Views");
            setSystem(true);
        }
        @Override
        protected IStatus run(IProgressMonitor monitor) {
            Pair<List<String>, List<String>> views = findViews(false);
            mCustomViews = views.getFirst();
            mThirdPartyViews = views.getSecond();

            // Notify listeners on SWT's UI thread
            Display.getDefault().asyncExec(new Runnable() {
                public void run() {
                    Collection<String> customViews =
                        Collections.unmodifiableCollection(mCustomViews);
                    Collection<String> thirdPartyViews =
                        Collections.unmodifiableCollection(mThirdPartyViews);
                    synchronized (this) {
                        for (Listener l : mListeners) {
                            l.viewsUpdated(customViews, thirdPartyViews);
                        }
                        mListeners.clear();
                        mRefreshing = false;
                    }
                }
            });
            return Status.OK_STATUS;
        }
    }
}
