/*
 * 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.*;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.netbeans.api.java.source.ModificationResult.Difference;
import org.netbeans.api.java.source.*;
import org.netbeans.modules.refactoring.api.AbstractRefactoring;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.java.api.UseSuperTypeRefactoring;
import org.netbeans.modules.refactoring.java.spi.DiffElement;
import org.netbeans.modules.refactoring.java.spi.JavaRefactoringPlugin;
import org.netbeans.modules.refactoring.java.spi.RefactoringVisitor;
import org.netbeans.modules.refactoring.java.spi.ToPhaseException;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;


/*
 * UseSuperTypeRefactoringPlugin.java
 *
 * Created on June 22, 2005
 *
 * @author Bharath Ravi Kumar
 */
/**
 * The plugin that performs the actual work on
 * behalf of the use super type refactoring
 */
public class UseSuperTypeRefactoringPlugin extends JavaRefactoringPlugin {

    private final UseSuperTypeRefactoring refactoring;

    /**
     * Creates a new instance of UseSuperTypeRefactoringPlugin
     * @param refactoring The refactoring to be used by this plugin
     */
    public UseSuperTypeRefactoringPlugin(UseSuperTypeRefactoring refactoring) {
        this.refactoring = refactoring;
    }

    /**
     * Prepares the underlying where used query & checks
     * for the visibility of the target type.
     */
    @Override
    public Problem prepare(RefactoringElementsBag refactoringElements) {
        TreePathHandle subClassHandle = refactoring.getTypeElement();
        replaceSubtypeUsages(subClassHandle, refactoringElements);
        return null;
    }

    @Override
    protected JavaSource getJavaSource(Phase p) {
        switch (p) {
            default:
                return JavaSource.forFileObject(refactoring.getTypeElement().getFileObject());
        }
    }

    /**
     *Checks whether the candidate element is a valid Type.
     *@return Problem The problem instance indicating that an invalid element was selected.
     */
    @Override
    public Problem preCheck() {
        cancelRequest = false;
        cancelRequested.set(false);
        //        Element subType = refactoring.getTypeElement();
        //        if(!(subType instanceof JavaClass)){
        //            String errMsg = NbBundle.getMessage(UseSuperTypeRefactoringPlugin.class,
        //                    "ERR_UseSuperType_InvalidElement"); // NOI18N
        //            return new Problem(true, errMsg);
        //        }
        return null;
    }

    /**
     * @return A problem indicating that no super type was selected.
     */
    @Override
    public Problem fastCheckParameters() {
        if (refactoring.getTargetSuperType() == null) {
            return new Problem(true, NbBundle.getMessage(UseSuperTypeRefactoringPlugin.class, "ERR_UseSuperTypeNoSuperType"));
        }
        return null;
    }

    /**
     * A no op. Returns null
     */
    @Override
    public Problem checkParameters() {
        return null;
    }

    //---------private  methods follow--------

    private void replaceSubtypeUsages(final TreePathHandle subClassHandle, final RefactoringElementsBag elemsBag) {
        JavaSource javaSrc = JavaSource.forFileObject(subClassHandle.getFileObject());
        try {
            javaSrc.runUserActionTask(new CancellableTask<CompilationController>() {
                @Override
                public void cancel() { }
                @Override
                public void run(CompilationController complController) throws IOException {
                    complController.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);

                    FileObject fo = subClassHandle.getFileObject();
                    ClasspathInfo classpathInfo = RefactoringUtils.getClasspathInfoFor(true, true, fo) ;
                    
                    ClassIndex clsIndx = classpathInfo.getClassIndex();
                    TypeElement javaClassElement = (TypeElement) subClassHandle.
                            resolveElement(complController);
                    EnumSet<ClassIndex.SearchKind> typeRefSearch = EnumSet.of(ClassIndex.SearchKind.TYPE_REFERENCES);
                    Set<FileObject> refFileObjSet = clsIndx.getResources(ElementHandle.create(javaClassElement), typeRefSearch, EnumSet.of(ClassIndex.SearchScope.SOURCE));

                    if (!refFileObjSet.isEmpty()) {
                        fireProgressListenerStart(AbstractRefactoring.PREPARE, refFileObjSet.size());
                        try{
                            Collection<ModificationResult> results = processFiles(refFileObjSet, new FindRefTask(subClassHandle, refactoring.getTargetSuperType()));
                            elemsBag.registerTransaction(createTransaction(results));
                            for (ModificationResult result : results) {
                                for (FileObject fileObj : result.getModifiedFileObjects()) {
                                    for (Difference diff : result.getDifferences(fileObj)) {
                                        String old = diff.getOldText();
                                        if (old != null) {
                                            elemsBag.add(refactoring, DiffElement.create(diff, fileObj, result));
                                        }
                                    }
                                }
                            }
                        }finally{
                            fireProgressListenerStop();
                        }
                    }
                }
            }, false);
        } catch (IOException ioex) {
            ioex.printStackTrace();
        }
    }

    private final class FindRefTask implements CancellableTask<WorkingCopy> {

        private final TreePathHandle subClassHandle;
        private final ElementHandle superClassHandle;

        private FindRefTask(TreePathHandle subClassHandle, ElementHandle superClassHandle) {
            this.subClassHandle = subClassHandle;
            this.superClassHandle = superClassHandle;
        }

        @Override
        public void cancel() {
        }

        @Override
        public void run(WorkingCopy compiler) throws Exception {
            try {
                if (compiler.toPhase(JavaSource.Phase.RESOLVED) != JavaSource.Phase.RESOLVED) {
                    return;
                }
                CompilationUnitTree cu = compiler.getCompilationUnit();
                if (cu == null) {
                    ErrorManager.getDefault().log(ErrorManager.ERROR, "compiler.getCompilationUnit() is null " + compiler); // NOI18N
                    return;
                }
                Element subClassElement = subClassHandle.resolveElement(compiler);
                Element superClassElement = superClassHandle.resolve(compiler);
                if (superClassElement == null) {
                    //superClassElement is not resolvable in this project
                    //do not replace
                    //#202030
                    return;
                }
                assert subClassElement != null;
                ReferencesVisitor findRefVisitor = new ReferencesVisitor(compiler, subClassElement, superClassElement);
                findRefVisitor.scan(compiler.getCompilationUnit(), subClassElement);
            } finally {
                fireProgressListenerStep();
            }
        }

    }

    private static class ReferencesVisitor extends RefactoringVisitor {

        private final TypeElement superTypeElement;
        private final TypeElement subTypeElement;

        private ReferencesVisitor(WorkingCopy workingCopy, Element subClassElement, Element superClassElement) {
            try {
                setWorkingCopy(workingCopy);
            } catch (ToPhaseException phase) {
                //should never be thrown;
                Exceptions.printStackTrace(phase);
            }
            this.superTypeElement = (TypeElement) superClassElement;
            this.subTypeElement = (TypeElement) subClassElement;
        }

        @Override
        public Tree visitMemberSelect(MemberSelectTree memSelTree, Element elemToFind) {
            Element elem = asElement(memSelTree);
            
            if ((elem != null) && isStatic(elem)) {
                Element expreElem = asElement(memSelTree.getExpression());
                //If a static member was referenced using the object instead 
                //of the class, don't handle it here.
                if(expreElem == null || ! (ElementKind.CLASS.equals(expreElem.getKind()) || 
                        ElementKind.INTERFACE.equals(expreElem.getKind()))){
                    return super.visitMemberSelect(memSelTree, elemToFind);
                }
                TypeElement type = (TypeElement) expreElem;
                if (!subTypeElement.equals(type)) {
                    return super.visitMemberSelect(memSelTree, elemToFind);
                }
                if (hidesSupTypeMember(elem, superTypeElement)) {
                    replaceType(memSelTree, superTypeElement);
                }
            }
            return super.visitMemberSelect(memSelTree, elemToFind);
        }

        
        @Override
        public Tree visitVariable(VariableTree varTree, Element elementToMatch) {
            TreePath treePath = getCurrentPath();
            VariableElement varElement = (VariableElement) workingCopy.
                    getTrees().getElement(treePath);


            //This check shouldn't be needed (ideally).
            if (varElement == null) {
                return super.visitVariable(varTree, elementToMatch);
            }
            TreePath parentPath = treePath.getParentPath();
            if(parentPath != null && parentPath.getLeaf().getKind() == Tree.Kind.CATCH) {
                // Do not change in catch statement
                return super.visitVariable(varTree, elementToMatch);
            }

            Types types = workingCopy.getTypes();
            TypeMirror varTypeErasure = types.erasure(varElement.asType());
            TypeMirror elToMatchErasure = types.erasure(subTypeElement.asType());
        
            if (types.isSameType(varTypeErasure, elToMatchErasure)) {
                
                //Check for overloaded methods
                boolean clashWithOverload = false;
                if(parentPath != null && parentPath.getLeaf().getKind() == Tree.Kind.METHOD) {
                    Trees trees = workingCopy.getTrees();
                    ExecutableElement parent = (ExecutableElement) trees.getElement(parentPath);
                    TreePath enclosing = JavaRefactoringUtils.findEnclosingClass(workingCopy, parentPath, true, true, true, true, true);
                    TypeElement typeEl = (TypeElement) (enclosing == null? null : trees.getElement(enclosing));
                    if(parent != null && typeEl != null) {
                        Name simpleName = parent.getSimpleName();
                        int size = parent.getParameters().size();
                        OUTER: for (ExecutableElement method : ElementFilter.methodsIn(workingCopy.getElements().getAllMembers(typeEl))) {
                            if (method != parent &&
                                method.getKind() == parent.getKind() &&
                                size == method.getParameters().size() &&
                                simpleName.contentEquals(method.getSimpleName())) {
                                for (int i = 0; i < parent.getParameters().size(); i++) {
                                    VariableElement par = parent.getParameters().get(i);
                                    TypeMirror parErasure = types.erasure(par.asType());
                                    TypeMirror par2Erasure = types.erasure(method.getParameters().get(i).asType());
                                    if(!types.isSameType(parErasure, par2Erasure)) {
                                        if(types.isAssignable(types.erasure(superTypeElement.asType()), par2Erasure)) {
                                            clashWithOverload = true;
                                            break OUTER;
                                        }
                                        if(types.isSubtype(parErasure, par2Erasure)) {
                                            clashWithOverload = true;
                                            break OUTER;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                if (!clashWithOverload && isReplaceCandidate(varElement)) {
                    replaceWithSuperType(treePath, varTree, varElement, superTypeElement);
                }
            }
            return super.visitVariable(varTree, elementToMatch);
        }

        // handles only simple situations like
        // ArrayList list = (ArrayList) getList();
        // where ArrayList is being substituted by a supertype
        @Override
        public Tree visitTypeCast(TypeCastTree castTree, Element elementToMatch) {
            TreePath path = getCurrentPath();
            Types types = workingCopy.getTypes();
            TypeMirror castTypeErasure = types.erasure(workingCopy.getTrees().getTypeMirror(path));
            TypeMirror elToMatchErasure = types.erasure(subTypeElement.asType());
            path = path.getParentPath();
            Element element = workingCopy.getTrees().getElement(path);
            if (element instanceof VariableElement && types.isSameType(castTypeErasure, elToMatchErasure)) {
                VariableElement varElement = (VariableElement)element;
                TypeMirror varTypeErasure = types.erasure(varElement.asType());
                if (types.isSameType(varTypeErasure, elToMatchErasure) && isReplaceCandidate(varElement)) {
                    TypeCastTree newTree = make.TypeCast(
                        make.Identifier(superTypeElement), castTree.getExpression());
                    rewrite(castTree, newTree);
                }
            }
            return super.visitTypeCast(castTree, elementToMatch);
        }
        
        private boolean hidesSupTypeMember(Element methElement, TypeElement superTypeElement) {
            Elements elements = workingCopy.getElements();
            List<? extends Element> containedElements = elements.getAllMembers(superTypeElement);
            for (Element elem : containedElements) {
                boolean isPresentInSuperType = methElement.equals(elem) || 
                        elements.hides(methElement, elem);
                if ((elem != null) && isStatic(elem) && isPresentInSuperType) {
                    return true;
                }
            }
            return false;
        }

        private boolean isReplaceCandidate(VariableElement varElement) {
            VarUsageVisitor varUsagesVisitor = new VarUsageVisitor(subTypeElement,
                    workingCopy, superTypeElement);
            varUsagesVisitor.scan(workingCopy.getCompilationUnit(), varElement);
            return varUsagesVisitor.isReplaceCandidate();
        }

        private boolean isStatic(Element element) {
            Set<Modifier> modifiers = element.getModifiers();
            return modifiers.contains(Modifier.STATIC);
        }

        private void replaceType(MemberSelectTree memSelTree, Element superTypeElement) {
            MemberSelectTree newTree = make.MemberSelect(
                    make.Identifier(superTypeElement), memSelTree.getIdentifier());
            rewrite(memSelTree, newTree);
        }

        private void replaceWithSuperType(TreePath path, VariableTree oldVarTree, VariableElement varElement, Element superTypeElement) {
            Types types = workingCopy.getTypes();
            TypeMirror supTypeErasure = types.erasure(superTypeElement.asType());
            DeclaredType varType = (DeclaredType) varElement.asType();
            TypeMirror theType = null;
            List<TypeMirror> supertypes = new LinkedList(types.directSupertypes(varType));
            while(!supertypes.isEmpty()) {
                TypeMirror supertype = supertypes.remove(0);
                if(types.isSameType(types.erasure(supertype), supTypeErasure)) {
                    theType = supertype;
                    break;
                }
                supertypes.addAll(types.directSupertypes(supertype));
            }
            
            if(theType == null) {
                theType = supTypeErasure;
            }
            Tree superTypeTree = make.Type(theType);
  
            ExpressionTree oldInitTree = oldVarTree.getInitializer();
            ModifiersTree oldModifiers = oldVarTree.getModifiers();
            Tree newTree = make.Variable(oldModifiers, oldVarTree.getName(), 
                    superTypeTree, oldInitTree);
            rewrite(oldVarTree, newTree);
        }

        private Element asElement(Tree tree) {
            Trees treeUtil = workingCopy.getTrees();
            TreePath treePath = treeUtil.getPath(workingCopy.getCompilationUnit(), tree);
            Element element = treeUtil.getElement(treePath);
            return element;
        }

    }
}
