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

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.manifest.descriptors.AndroidManifestDescriptors;
import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiTextAttributeNode;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.xml.AndroidManifest;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
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 org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.ui.IJavaElementSearchConstants;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.SelectionDialog;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.TableWrapData;
import org.w3c.dom.Element;

import java.util.ArrayList;

/**
 * Represents an XML attribute for a class, that can be modified using a simple text field or
 * a dialog to choose an existing class. Also, there's a link to create a new class.
 * <p/>
 * See {@link UiTextAttributeNode} for more information.
 */
public class UiClassAttributeNode extends UiTextAttributeNode {

    private String mReferenceClass;
    private IPostTypeCreationAction mPostCreationAction;
    private boolean mMandatory;
    private final boolean mDefaultToProjectOnly;

    private class HierarchyTypeSelection extends TypeSelectionExtension {

        private IJavaProject mJavaProject;
        private IType mReferenceType;
        private Button mProjectOnly;
        private boolean mUseProjectOnly;

        public HierarchyTypeSelection(IProject project, String referenceClass)
                throws JavaModelException {
            mJavaProject = JavaCore.create(project);
            mReferenceType = mJavaProject.findType(referenceClass);
        }

        @Override
        public ITypeInfoFilterExtension getFilterExtension() {
            return new ITypeInfoFilterExtension() {
                public boolean select(ITypeInfoRequestor typeInfoRequestor) {

                    boolean projectOnly = mUseProjectOnly;

                    String packageName = typeInfoRequestor.getPackageName();
                    String typeName = typeInfoRequestor.getTypeName();
                    String enclosingType = typeInfoRequestor.getEnclosingName();

                    // build the full class name.
                    StringBuilder sb = new StringBuilder(packageName);
                    sb.append('.');
                    if (enclosingType.length() > 0) {
                        sb.append(enclosingType);
                        sb.append('.');
                    }
                    sb.append(typeName);

                    String className = sb.toString();

                    try {
                        IType type = mJavaProject.findType(className);

                        if (type == null) {
                            return false;
                        }

                        // don't display abstract classes
                        if ((type.getFlags() & Flags.AccAbstract) != 0) {
                            return false;
                        }

                        // if project-only is selected, make sure the package fragment is
                        // an actual source (thus "from this project").
                        if (projectOnly) {
                            IPackageFragment frag = type.getPackageFragment();
                            if (frag == null || frag.getKind() != IPackageFragmentRoot.K_SOURCE) {
                                return false;
                            }
                        }

                        // get the type hierarchy and reference type is one of the super classes.
                        ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
                                new NullProgressMonitor());

                        IType[] supertypes = hierarchy.getAllSupertypes(type);
                        int n = supertypes.length;
                        for (int i = 0; i < n; i++) {
                            IType st = supertypes[i];
                            if (mReferenceType.equals(st)) {
                                return true;
                            }
                        }
                    } catch (JavaModelException e) {
                    }

                    return false;
                }
            };
        }

        @Override
        public Control createContentArea(Composite parent) {

            mProjectOnly = new Button(parent, SWT.CHECK);
            mProjectOnly.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            mProjectOnly.setText(String.format("Display classes from sources of project '%s' only",
                    mJavaProject.getProject().getName()));

            mUseProjectOnly = mDefaultToProjectOnly;
            mProjectOnly.setSelection(mUseProjectOnly);

            mProjectOnly.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    super.widgetSelected(e);
                    mUseProjectOnly = mProjectOnly.getSelection();
                    getTypeSelectionComponent().triggerSearch();
                }
            });

            return super.createContentArea(parent);
        }
    }

    /**
     * Classes which implement this interface provide a method processing newly created classes.
     */
    public static interface IPostTypeCreationAction {
        /**
         * Sent to process a newly created class.
         * @param newType the IType representing the newly created class.
         */
        public void processNewType(IType newType);
    }

    /**
     * Creates a {@link UiClassAttributeNode} object that will display ui to select or create
     * classes.
     * @param referenceClass The allowed supertype of the classes that are to be selected
     * or created. Can be null.
     * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
     * modification of the class.
     * @param mandatory indicates if the class value is mandatory
     * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
     * @param defaultToProjectOnly When true display classes of this project only by default.
     *         When false any class path will be considered. The user can always toggle this.
     */
    public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
            boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent,
            boolean defaultToProjectOnly) {
        super(attributeDescriptor, uiParent);

        mReferenceClass = referenceClass;
        mPostCreationAction = postCreationAction;
        mMandatory = mandatory;
        mDefaultToProjectOnly = defaultToProjectOnly;
    }

    /* (non-java doc)
     * Creates a label widget and an associated text field.
     * <p/>
     * As most other parts of the android manifest editor, this assumes the
     * parent uses a table layout with 2 columns.
     */
    @Override
    public void createUiControl(final Composite parent, IManagedForm managedForm) {
        setManagedForm(managedForm);
        FormToolkit toolkit = managedForm.getToolkit();
        TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();

        StringBuilder label = new StringBuilder();
        label.append("<form><p><a href='unused'>");
        label.append(desc.getUiName());
        label.append("</a></p></form>");
        FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
                label.toString(), true /* setupLayoutData */);
        formText.addHyperlinkListener(new HyperlinkAdapter() {
            @Override
            public void linkActivated(HyperlinkEvent e) {
                super.linkActivated(e);
                handleLabelClick();
            }
        });
        formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
        SectionHelper.addControlTooltip(formText, desc.getTooltip());

        Composite composite = toolkit.createComposite(parent);
        composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
        GridLayout gl = new GridLayout(2, false);
        gl.marginHeight = gl.marginWidth = 0;
        composite.setLayout(gl);
        // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
        // for the text field below
        toolkit.paintBordersFor(composite);

        final Text text = toolkit.createText(composite, getCurrentValue());
        GridData gd = new GridData(GridData.FILL_HORIZONTAL);
        gd.horizontalIndent = 1;  // Needed by the fixed composite borders under GTK
        text.setLayoutData(gd);
        Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);

        setTextWidget(text);

        browseButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                super.widgetSelected(e);
                handleBrowseClick();
            }
        });
    }

    /* (non-java doc)
     *
     * Add a modify listener that will check the validity of the class
     */
    @Override
    protected void onAddValidators(final Text text) {
        ModifyListener listener = new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                try {
                    String textValue = text.getText().trim();
                    if (textValue.length() == 0) {
                        if (mMandatory) {
                            setErrorMessage("Value is mandatory", text);
                        } else {
                            setErrorMessage(null, text);
                        }
                        return;
                    }
                    // first we need the current java package.
                    String javaPackage = getManifestPackage();

                    // build the fully qualified name of the class
                    String className = AndroidManifest.combinePackageAndClassName(
                            javaPackage, textValue);

                    // only test the vilibility for activities.
                    boolean testVisibility = SdkConstants.CLASS_ACTIVITY.equals(
                            mReferenceClass);

                    // test the class
                    setErrorMessage(BaseProjectHelper.testClassForManifest(
                            BaseProjectHelper.getJavaProject(getProject()), className,
                            mReferenceClass, testVisibility), text);
                } catch (CoreException ce) {
                    setErrorMessage(ce.getMessage(), text);
                }
            }
        };

        text.addModifyListener(listener);

        // Make sure the validator removes its message(s) when the widget is disposed
        text.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                // we don't want to use setErrorMessage, because we don't want to reset
                // the error flag in the UiAttributeNode
                getManagedForm().getMessageManager().removeMessage(text, text);
            }
        });

        // Finally call the validator once to make sure the initial value is processed
        listener.modifyText(null);
    }

    private void handleBrowseClick() {
        Text text = getTextWidget();

        // we need to get the project of the manifest.
        IProject project = getProject();
        if (project != null) {

            // Create a search scope including only the source folder of the current
            // project.
            IPackageFragmentRoot[] packageFragmentRoots = getPackageFragmentRoots(project,
                    true /*include_containers*/);
            IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
                    packageFragmentRoots,
                    false);

            try {
                SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
                    PlatformUI.getWorkbench().getProgressService(),
                    scope,
                    IJavaElementSearchConstants.CONSIDER_CLASSES,  // style
                    false, // no multiple selection
                    "**",  //$NON-NLS-1$ //filter
                    new HierarchyTypeSelection(project, mReferenceClass));
                dlg.setMessage(String.format("Select class name for element %1$s:",
                        getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
                if (dlg instanceof ITypeSelectionComponent) {
                    ((ITypeSelectionComponent)dlg).triggerSearch();
                }

                if (dlg.open() == Window.OK) {
                    Object[] results = dlg.getResult();
                    if (results.length == 1) {
                        handleNewType((IType)results[0]);
                    }
                }
            } catch (JavaModelException e1) {
                AdtPlugin.log(e1, "UiClassAttributeNode HandleBrowser failed");
            }
        }
    }

    private void handleLabelClick() {
        // get the current value
        String className = getTextWidget().getText().trim();

        // get the package name from the manifest.
        String packageName = getManifestPackage();

        if (className.length() == 0) {
            createNewClass(packageName, null /* className */);
        } else {
            // build back the fully qualified class name.
            String fullClassName = className;
            if (className.startsWith(".")) { //$NON-NLS-1$
                fullClassName = packageName + className;
            } else {
                String[] segments = className.split(AdtConstants.RE_DOT);
                if (segments.length == 1) {
                    fullClassName = packageName + "." + className; //$NON-NLS-1$
                }
            }

            // in case the type is enclosed, we need to replace the $ with .
            fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$

            // now we try to find the file that contains this class and we open it in the editor.
            IProject project = getProject();
            IJavaProject javaProject = JavaCore.create(project);

            try {
                IType result = javaProject.findType(fullClassName);
                if (result != null) {
                    JavaUI.openInEditor(result);
                } else {
                    // split the last segment from the fullClassname
                    int index = fullClassName.lastIndexOf('.');
                    if (index != -1) {
                        createNewClass(fullClassName.substring(0, index),
                                fullClassName.substring(index+1));
                    } else {
                        createNewClass(packageName, className);
                    }
                }
            } catch (JavaModelException e) {
                AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
            } catch (PartInitException e) {
                AdtPlugin.log(e, "UiClassAttributeNode HandleLabel failed");
            }
        }
    }

    private IProject getProject() {
        UiElementNode uiNode = getUiParent();
        AndroidXmlEditor editor = uiNode.getEditor();
        IEditorInput input = editor.getEditorInput();
        if (input instanceof IFileEditorInput) {
            // from the file editor we can get the IFile object, and from it, the IProject.
            IFile file = ((IFileEditorInput)input).getFile();
            return file.getProject();
        }

        return null;
    }


    /**
     * Returns the current value of the /manifest/package attribute.
     * @return the package or an empty string if not found
     */
    private String getManifestPackage() {
        // get the root uiNode to get the 'package' attribute value.
        UiElementNode rootNode = getUiParent().getUiRoot();

        Element xmlElement = (Element) rootNode.getXmlNode();

        if (xmlElement != null) {
            return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
        }
        return ""; //$NON-NLS-1$
    }


    /**
     * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
     * the specified project.
     * @param project the project
     * @param include_containers True to include containers
     * @return an array of IPackageFragmentRoot.
     */
    private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project,
            boolean include_containers) {
        ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
        try {
            IJavaProject javaProject = JavaCore.create(project);
            IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
            for (int i = 0; i < roots.length; i++) {
                IClasspathEntry entry = roots[i].getRawClasspathEntry();
                if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE ||
                        (include_containers &&
                                entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER)) {
                    result.add(roots[i]);
                }
            }
        } catch (JavaModelException e) {
        }

        return result.toArray(new IPackageFragmentRoot[result.size()]);
    }

    private void handleNewType(IType type) {
        Text text = getTextWidget();

        // get the fully qualified name with $ to properly detect the enclosing types.
        String name = type.getFullyQualifiedName('$');

        String packageValue = getManifestPackage();

        // check if the class doesn't start with the package.
        if (packageValue.length() > 0 && name.startsWith(packageValue)) {
            // if it does, we remove the package and the first dot.
            name = name.substring(packageValue.length() + 1);

            // look for how many segments we have left.
            // if one, just write it that way.
            // if more than one, write it with a leading dot.
            String[] packages = name.split(AdtConstants.RE_DOT);
            if (packages.length == 1) {
                text.setText(name);
            } else {
                text.setText("." + name); //$NON-NLS-1$
            }
        } else {
            text.setText(name);
        }
    }

    private void createNewClass(String packageName, String className) {
        // create the wizard page for the class creation, and configure it
        NewClassWizardPage page = new NewClassWizardPage();

        // set the parent class
        page.setSuperClass(mReferenceClass, true /* canBeModified */);

        // get the source folders as java elements.
        IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject(),
                true /*include_containers*/);

        IPackageFragmentRoot currentRoot = null;
        IPackageFragment currentFragment = null;
        int packageMatchCount = -1;

        for (IPackageFragmentRoot root : roots) {
            // Get the java element for the package.
            // This method is said to always return a IPackageFragment even if the
            // underlying folder doesn't exist...
            IPackageFragment fragment = root.getPackageFragment(packageName);
            if (fragment != null && fragment.exists()) {
                // we have a perfect match! we use it.
                currentRoot = root;
                currentFragment = fragment;
                packageMatchCount = -1;
                break;
            } else {
                // we don't have a match. we look for the fragment with the best match
                // (ie the closest parent package we can find)
                try {
                    IJavaElement[] children;
                    children = root.getChildren();
                    for (IJavaElement child : children) {
                        if (child instanceof IPackageFragment) {
                            fragment = (IPackageFragment)child;
                            if (packageName.startsWith(fragment.getElementName())) {
                                // its a match. get the number of segments
                                String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
                                if (segments.length > packageMatchCount) {
                                    packageMatchCount = segments.length;
                                    currentFragment = fragment;
                                    currentRoot = root;
                                }
                            }
                        }
                    }
                } catch (JavaModelException e) {
                    // Couldn't get the children: we just ignore this package root.
                }
            }
        }

        ArrayList<IPackageFragment> createdFragments = null;

        if (currentRoot != null) {
            // if we have a perfect match, we set it and we're done.
            if (packageMatchCount == -1) {
                page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
                page.setPackageFragment(currentFragment, true /* canBeModified */);
            } else {
                // we have a partial match.
                // create the package. We have to start with the first segment so that we
                // know what to delete in case of a cancel.
                try {
                    createdFragments = new ArrayList<IPackageFragment>();

                    int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
                    int count = 0;
                    int index = -1;
                    // skip the matching packages
                    while (count < packageMatchCount) {
                        index = packageName.indexOf('.', index+1);
                        count++;
                    }

                    // create the rest of the segments, except for the last one as indexOf will
                    // return -1;
                    while (count < totalCount - 1) {
                        index = packageName.indexOf('.', index+1);
                        count++;
                        createdFragments.add(currentRoot.createPackageFragment(
                                packageName.substring(0, index),
                                true /* force*/, new NullProgressMonitor()));
                    }

                    // create the last package
                    createdFragments.add(currentRoot.createPackageFragment(
                            packageName, true /* force*/, new NullProgressMonitor()));

                    // set the root and fragment in the Wizard page
                    page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
                    page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
                            true /* canBeModified */);
                } catch (JavaModelException e) {
                    // if we can't create the packages, there's a problem. we revert to the default
                    // package
                    for (IPackageFragmentRoot root : roots) {
                        // Get the java element for the package.
                        // This method is said to always return a IPackageFragment even if the
                        // underlying folder doesn't exist...
                        IPackageFragment fragment = root.getPackageFragment(packageName);
                        if (fragment != null && fragment.exists()) {
                            page.setPackageFragmentRoot(root, true /* canBeModified*/);
                            page.setPackageFragment(fragment, true /* canBeModified */);
                            break;
                        }
                    }
                }
            }
        } else if (roots.length > 0) {
            // if we haven't found a valid fragment, we set the root to the first source folder.
            page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
        }

        // if we have a starting class name we use it
        if (className != null) {
            page.setTypeName(className, true /* canBeModified*/);
        }

        // create the action that will open it the wizard.
        OpenNewClassWizardAction action = new OpenNewClassWizardAction();
        action.setConfiguredWizardPage(page);
        action.run();
        IJavaElement element = action.getCreatedElement();

        if (element != null) {
            if (element.getElementType() == IJavaElement.TYPE) {

                IType type = (IType)element;

                if (mPostCreationAction != null) {
                    mPostCreationAction.processNewType(type);
                }

                handleNewType(type);
            }
        } else {
            // lets delete the packages we created just for this.
            // we need to start with the leaf and go up
            if (createdFragments != null) {
                try {
                    for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
                        createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
                    }
                } catch (JavaModelException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Sets the error messages. If message is <code>null</code>, the message is removed.
     * @param message the message to set, or <code>null</code> to remove the current message
     * @param textWidget the {@link Text} widget associated to the message.
     */
    private final void setErrorMessage(String message, Text textWidget) {
        if (message != null) {
            setHasError(true);
            getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
                    IMessageProvider.ERROR, textWidget);
        } else {
            setHasError(false);
            getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
        }
    }

    @Override
    public String[] getPossibleValues(String prefix) {
        // TODO: compute a list of existing classes for content assist completion
        return null;
    }
}

