/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.netbeans.modules.refactoring.java.plugins;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.*;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.swing.Action;
import org.netbeans.api.fileinfo.NonRecursiveFolder;
import org.netbeans.api.java.source.*;
import org.netbeans.modules.refactoring.api.*;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants;
import org.netbeans.modules.refactoring.java.spi.JavaRefactoringPlugin;
import org.netbeans.modules.refactoring.java.ui.WhereUsedQueryUI;
import org.netbeans.modules.refactoring.java.ui.tree.ElementGrip;
import org.netbeans.modules.refactoring.spi.ProblemDetailsFactory;
import org.netbeans.modules.refactoring.spi.ProblemDetailsImplementation;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.netbeans.modules.refactoring.spi.ui.RefactoringUI;
import org.netbeans.modules.refactoring.spi.ui.UI;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.util.Cancellable;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.lookup.Lookups;
import static org.netbeans.modules.refactoring.java.plugins.Bundle.*;


/**
 * The plugin that carries out Safe Delete refactoring.
 * @author Bharath Ravikumar
 * @author Jan Becicka
 */
public class SafeDeleteRefactoringPlugin extends JavaRefactoringPlugin {
    
    private static final String DOT = "."; //NOI18N
    private static final String JAVA_EXTENSION = "java";
    private final ArrayList<TreePathHandle> grips;
    private final SafeDeleteRefactoring refactoring;
    private WhereUsedQuery[] whereUsedQueries;
    
    /**
     * Creates the a new instance of the Safe Delete refactoring
     * plugin.
     * @param refactoring The refactoring to be used by this plugin
     */
    public SafeDeleteRefactoringPlugin(SafeDeleteRefactoring refactoring) {
        this.refactoring = refactoring;
        this.grips = new ArrayList<>();
    }

    /**
     * Invokes the checkParameters of each of the underlying
     * WhereUsed refactorings and returns a Problem (if any)
     * returned by any of these queries.
     */
    @Override
    public Problem checkParameters() {
        //This class expects too many details from SafeDeleteRefactoring
        //But there's no other go I guess.
        grips.clear();
        for (final FileObject f: lookupJavaFileObjects()) {
            JavaSource source = JavaSource.forFileObject(f);
            if (source == null) {
                continue;
            }
            try {
                source.runUserActionTask(new CancellableTask<CompilationController>() {
                    @Override
                    public void cancel() {
                        
                    }
                    @Override
                    public void run(CompilationController co) throws Exception {
                        co.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
                        CompilationUnitTree cut = co.getCompilationUnit();
                        for (Tree t: cut.getTypeDecls()) {
                            if (t.getKind() == Tree.Kind.EMPTY_STATEMENT) {
                                // syntax errors
                                continue;
                            }
                            TreePathHandle handle = TreePathHandle.create(TreePath.getPath(cut, t), co);
                            if (!containsHandle(handle, co)) {
                                grips.add(handle);
                            }
                        }
                    }
                }, true);
            } catch (IllegalArgumentException | IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        
        grips.addAll(refactoring.getRefactoringSource().lookupAll(TreePathHandle.class));
        
        whereUsedQueries = new WhereUsedQuery[grips.size()];
        for(int i = 0;i <  whereUsedQueries.length; ++i) {
            final TreePathHandle handle = grips.get(i);
            final WhereUsedQuery q = new WhereUsedQuery(Lookups.singleton(handle));
            for (Object o:refactoring.getContext().lookupAll(Object.class)) {
                q.getContext().add(o);
            }
            q.getContext().add(refactoring);
            q.getContext().add(this);
            
            if(Tree.Kind.METHOD == handle.getKind()) {
                JavaSource source;
                source = JavaSource.forFileObject(handle.getFileObject());
                try {
                    final int index = i;
                    source.runUserActionTask(new Task<CompilationController>() {

                        @Override
                        public void run(CompilationController info) throws Exception {
                            info.toPhase(JavaSource.Phase.RESOLVED);
                            final Element element = handle.resolveElement(info);
                            if (element == null) {
                                throw new NullPointerException(String.format("#145291: Cannot resolve handle: %s\n%s", handle, info.getClasspathInfo())); // NOI18N
                            }
                            ElementKind kind = element.getKind();
                            if (kind == ElementKind.METHOD) {
                                Collection<ExecutableElement> overridens = JavaRefactoringUtils.getOverriddenMethods((ExecutableElement)element, info);
                                if(!overridens.isEmpty()) {
                                    ExecutableElement el = (ExecutableElement) overridens.iterator().next();
                                    assert el!=null;
                                    TreePathHandle basem = TreePathHandle.create(el, info);
                                    q.setRefactoringSource(Lookups.fixed(basem));
                                    grips.remove(index);
                                    grips.add(index, basem);
                                }
                            }
                        }
                    }, true);
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
                q.putValue(WhereUsedQueryConstants.SEARCH_FROM_BASECLASS, true);
                q.putValue(WhereUsedQueryConstants.FIND_OVERRIDING_METHODS, false);
            }
            
            q.putValue(WhereUsedQuery.SEARCH_IN_COMMENTS, refactoring.isCheckInComments());
            whereUsedQueries[i] = q;
        }
        
        Problem problemFromUsage;
        for(int i = 0;i < whereUsedQueries.length; ++i) {
//          Fix for issue 63050. Doesn't make sense to check usages of a Resource.Ignore it.
//            if(whereUsedQueries[i].getRefactoredObject() instanceof Resource)
//                continue;
            problemFromUsage = whereUsedQueries[i].checkParameters();
            if(problemFromUsage != null) {
                return problemFromUsage;
            }
        }
        return null;
    }

    /**
     * A No-op for this particular refactoring.
     */
    @Override
    public Problem fastCheckParameters() {
        //Nothing to be done for Safe Delete
        return null;
    }

    /**
     * Checks whether the element being refactored is a valid Method/Field/Class
     * @return Problem returns a generic problem message if the check fails
     */
    @Override
    @NbBundle.Messages({"# {0} - VariableName", "ERR_VarNotInBlockOrMethod=Variable \"{0}\" is not inside a block or method declaration."})
    public Problem preCheck() {
        cancelRequest = false;
        cancelRequested.set(false);
        final Problem[] problem = new Problem[1];
        Collection<? extends TreePathHandle> handles = refactoring.getRefactoringSource().lookupAll(TreePathHandle.class);
        for (final TreePathHandle tph : handles) {
            final FileObject fileObject = tph.getFileObject();
            if (fileObject == null || !fileObject.isValid()) {
                return new Problem(true, NbBundle.getMessage(FindVisitor.class, "DSC_ElNotAvail")); // NOI18N
            }
            JavaSource js = JavaSource.forFileObject(fileObject);
            if (js==null) {
                continue;
            }
            try {
                js.runUserActionTask(new Task<CompilationController>() {
                    
                    @Override
                    public void run(CompilationController javac) throws Exception {
                        javac.toPhase(JavaSource.Phase.RESOLVED);
                        TreePath selectedTree = tph.resolve(javac);
                        if (selectedTree != null && selectedTree.getParentPath() != null
                                && !TreeUtilities.CLASS_TREE_KINDS.contains(selectedTree.getParentPath().getLeaf().getKind())
                                && selectedTree.getParentPath().getLeaf().getKind() != Tree.Kind.COMPILATION_UNIT
                                && selectedTree.getLeaf().getKind() == Tree.Kind.VARIABLE) {
                            switch (selectedTree.getParentPath().getLeaf().getKind()) {
                                case BLOCK:
                                case METHOD:
                                    break;
                                default:
                                    problem[0] = new Problem(true, ERR_VarNotInBlockOrMethod(selectedTree.getLeaf().toString()));
                            }
                        }
                    }
                }, true);
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
        return problem[0];
    }

    
    /**
     * For each element to be refactored, the corresponding
     * prepare method of the underlying WhereUsed query is
     * invoked to check for usages. If none is present, a
     * <CODE>SafeDeleteRefactoringElement</CODE> is created
     * with the corresponding element.
     * @param refactoringElements
     * @return
     */
    @Override
    public Problem prepare(RefactoringElementsBag refactoringElements) {
        RefactoringSession usages = RefactoringSession.create("delete"); // NOI18N
        Set<TreePathHandle> refactoredObjects = new HashSet<>();
        Collection<? extends FileObject> files = lookupJavaFileObjects();
        Problem problem = findUsagesAndDelete(refactoredObjects, usages, files, refactoringElements);
        if(problem != null && problem.isFatal()) {
            return problem;
        }
        
        for (RefactoringElement refacElem : usages.getRefactoringElements()) {
            final ElementGrip elem = refacElem.getLookup().lookup(ElementGrip.class);
            if (files.contains(refacElem.getParentFile())) {
                continue;
            }
            if (!isPendingDelete(elem, refactoredObjects)) {
                problem = JavaPluginUtils.chainProblems(problem, new Problem(false, NbBundle.getMessage(SafeDeleteRefactoringPlugin.class, "ERR_ReferencesFound"),
                        ProblemDetailsFactory.createProblemDetails(
                                new ProblemDetailsImplemen(new WhereUsedQueryUI(
                                        elem!=null?elem.getHandle():null, getWhereUsedItemNames(), refactoring),
                                        usages))));
                break;
            }
        }
        fireProgressListenerStop();
        return problem;
    }

    private boolean containsHandle(TreePathHandle handle, CompilationInfo info) {
        Element wanted = handle.resolveElement(info);
        for (TreePathHandle current : refactoring.getRefactoringSource().lookupAll(TreePathHandle.class)) {
            if (wanted == current.resolveElement(info)) {
                return true;
            }
        }
        return false;
    }
    
    private Problem findUsagesAndDelete(Set<TreePathHandle> refactoredObjects, RefactoringSession usages, Collection<? extends FileObject> files, RefactoringElementsBag refactoringElements) throws IllegalArgumentException {
        fireProgressListenerStart(AbstractRefactoring.PARAMETERS_CHECK, whereUsedQueries.length + 1);
        Problem problem = null;
        try {
            for (int i = 0; i < whereUsedQueries.length; ++i) {
                TreePathHandle refactoredObject = whereUsedQueries[i].getRefactoringSource().lookup(TreePathHandle.class);
                refactoredObjects.add(refactoredObject);
                whereUsedQueries[i].prepare(usages);
                TreePathHandle treePathHandle = grips.get(i);
                Set<FileObject> relevant = getRelevantFiles(treePathHandle);
                if(Tree.Kind.METHOD == treePathHandle.getKind()) {
                    OverriddenAbsMethodFinder finder = new OverriddenAbsMethodFinder(allMethods);
                    JavaSource javaSrc = JavaSource.forFileObject(treePathHandle.getFileObject());
                    try {
                        javaSrc.runUserActionTask(finder, true);
                    } catch (IOException ioException) {
                        ErrorManager.getDefault().notify(cancelRequested.get()?ErrorManager.INFORMATIONAL:ErrorManager.UNKNOWN,ioException);
                    }
                    problem = finder.getProblem();
                }
                TransformTask task = new TransformTask(new DeleteTransformer(allMethods, files), treePathHandle);
                problem = JavaPluginUtils.chainProblems(createAndAddElements(relevant, task, refactoringElements, refactoring), problem);
                fireProgressListenerStep();
            }
        } finally {
            usages.finished();
        }
        
        return problem;
    }

    private HashSet<ElementHandle<ExecutableElement>> allMethods;
    private Set<FileObject> getRelevantFiles(final TreePathHandle tph) {
        if (tph.getKind() == Kind.METHOD) {
            ClasspathInfo cpInfo = RefactoringUtils.getClasspathInfoFor(tph);
            final Set<FileObject> set = new LinkedHashSet<>();
            set.add(tph.getFileObject());
            JavaSource source = JavaSource.create(cpInfo, tph.getFileObject());

            try {
                source.runUserActionTask(new CancellableTask<CompilationController>() {

                    @Override
                    public void cancel() {
                        throw new UnsupportedOperationException("Not supported yet."); // NOI18N
                    }

                    @Override
                    public void run(CompilationController info) throws Exception {
                        final ClassIndex idx = info.getClasspathInfo().getClassIndex();
                        info.toPhase(JavaSource.Phase.RESOLVED);
                        final ElementUtilities elmUtils = info.getElementUtilities();

                        //add all references of overriding methods
                        ExecutableElement el = (ExecutableElement) tph.resolveElement(info);
                        ElementHandle<ExecutableElement> methodHandle = ElementHandle.create(el);
                        ElementHandle<TypeElement> enclosingType = ElementHandle.create(elmUtils.enclosingTypeElement(el));
                        allMethods = new HashSet<>();
                        allMethods.add(methodHandle);
                        for (ExecutableElement e : JavaRefactoringUtils.getOverridingMethods(el, info, cancelRequested)) {
                            ElementHandle<ExecutableElement> handle = ElementHandle.create(e);
                            set.add(SourceUtils.getFile(handle, info.getClasspathInfo()));
                            ElementHandle<TypeElement> encl = ElementHandle.create(elmUtils.enclosingTypeElement(e));
                            set.addAll(idx.getResources(encl, EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES), EnumSet.of(ClassIndex.SearchScope.SOURCE)));
                            allMethods.add(ElementHandle.create(e));
                        }
                        //add all references of overriden methods
                        for (ExecutableElement e : JavaRefactoringUtils.getOverriddenMethods(el, info)) {
                            ElementHandle<ExecutableElement> handle = ElementHandle.create(e);
                            set.add(SourceUtils.getFile(handle, info.getClasspathInfo()));
                            ElementHandle<TypeElement> encl = ElementHandle.create(elmUtils.enclosingTypeElement(e));
                            set.addAll(idx.getResources(encl, EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES), EnumSet.of(ClassIndex.SearchScope.SOURCE)));
                            allMethods.add(ElementHandle.create(e));
                        }
                        set.addAll(idx.getResources(enclosingType, EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES), EnumSet.of(ClassIndex.SearchScope.SOURCE)));
                        set.add(SourceUtils.getFile(methodHandle, info.getClasspathInfo()));
                    }
                }, true);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }
            return set;
        } else {
            return Collections.singleton(tph.getFileObject());
        }
    }
    

    private String getWhereUsedItemNames() {
        final StringBuilder b = new StringBuilder();
        for (final TreePathHandle handle:grips) {
            try {
                JavaSource.forFileObject(handle.getFileObject()).runUserActionTask(new Task<CompilationController>() {
                    @Override
                    public void run(CompilationController parameter) throws Exception {
                        parameter.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
                        if (b.length() > 0) {
                            b.append(", ");
                        }
                        b.append(handle.resolveElement(parameter).getSimpleName());
                    }
                }, true);
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return b.toString();
    }
    
    private Collection<? extends FileObject> lookupJavaFileObjects() {
        Lookup lkp = refactoring.getRefactoringSource();
        Collection<? extends FileObject> javaFiles;
        NonRecursiveFolder folder = lkp.lookup(NonRecursiveFolder.class);
        if (folder != null) {
            javaFiles = getJavaFileObjects(folder.getFolder(), false);
        } else {
            Collection<FileObject> javaFileObjects =  new ArrayList<>();
            for (FileObject fileObject : lkp.lookupAll(FileObject.class)) {
                if (fileObject.isFolder()) {
                    javaFileObjects.addAll(getJavaFileObjects(fileObject, true));
                }else if (JavaRefactoringUtils.isRefactorable(fileObject)) {
                    javaFileObjects.add(fileObject);
                }
            }
            javaFiles = javaFileObjects;
        }
        return javaFiles;
    }
    
    @Override
    protected JavaSource getJavaSource(Phase p) {
        return null;
    }
    
    private static void addSourcesInDir(FileObject dirFileObject, boolean isRecursive, Collection<FileObject> javaSrcFiles){
        for (FileObject childFileObject : dirFileObject.getChildren()) {
            if (childFileObject.isData() && JAVA_EXTENSION.equalsIgnoreCase(childFileObject.getExt())) {
                javaSrcFiles.add(childFileObject);
            }
            else if (childFileObject.isFolder() && isRecursive) {
                addSourcesInDir(childFileObject, true, javaSrcFiles);
            }
        }
    }
    
    private static Collection<FileObject> getJavaFileObjects(FileObject dirFileObject, boolean isRecursive){
        Collection<FileObject> javaSrcFiles = new ArrayList<>();
        addSourcesInDir(dirFileObject, isRecursive, javaSrcFiles);
        return javaSrcFiles;
    }
    
    private static String getMethodName(final TreePathHandle methodHandle){
        JavaSource javaSrc = JavaSource.forFileObject(methodHandle.getFileObject());
        final String[] methodNameString = new String[1];
        //Ugly hack to return the method name from the anonymous inner class
        try {
            javaSrc.runUserActionTask(new CancellableTask<CompilationController>(){

                @Override
                public void cancel() {
                    //No op
                }

                @Override
                public void run(CompilationController compilationController) throws Exception {
                    ExecutableElement execElem = (ExecutableElement) methodHandle.resolveElement(compilationController);
                    TypeElement type = (TypeElement) execElem.getEnclosingElement();
                    methodNameString[0] = type.getQualifiedName() + DOT + execElem.toString();
                }
                
            }, true);

        } catch (IOException ioException) {
            ErrorManager.getDefault().notify(ioException);
        }

        return methodNameString[0];
    }
    
    private static boolean isPendingDelete(ElementGrip elementGrip, Set<TreePathHandle> refactoredObjects) {
        ElementGrip parent = elementGrip;
        if (parent!=null) {
            do {
                if (refactoredObjects.contains(parent.getHandle())) {
                    return true;
                }
                parent = parent.getParent();
            } while (parent!=null);
        }
        return false;
    }
    
    private class ProblemDetailsImplemen implements ProblemDetailsImplementation {

        private RefactoringUI ui;
        private RefactoringSession rs;
        
        public ProblemDetailsImplemen(RefactoringUI ui, RefactoringSession rs) {
            this.ui = ui;
            this.rs = rs;
        }
        
        @Override
        public void showDetails(Action callback, Cancellable parent) {
            parent.cancel();
            UI.openRefactoringUI(ui, rs, callback);
        }
        
        @Override
        public String getDetailsHint() {
            return NbBundle.getMessage(SafeDeleteRefactoringPlugin.class, "LBL_ShowUsages");
        }
    }
}
