/*
 * 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.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeKind;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.api.ProgressEvent;
import org.netbeans.modules.refactoring.api.RenameRefactoring;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.spi.JavaRefactoringPlugin;
import org.netbeans.modules.refactoring.java.ui.JavaRenameProperties;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.netbeans.spi.gototest.TestLocator;
import org.netbeans.spi.gototest.TestLocator.LocationResult;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.lookup.Lookups;

/**
 *
 * @author Ralph Ruijs
 */
public class RenameTestClassRefactoringPlugin extends JavaRefactoringPlugin {

    public static final EnumSet<ElementKind> SUPPORTED = EnumSet.of(ElementKind.CLASS, ElementKind.ENUM, ElementKind.INTERFACE, ElementKind.ANNOTATION_TYPE, ElementKind.METHOD);
    private RenameRefactoring refactoring;
    private TreePathHandle treePathHandle;
    private RenameRefactoring[] renameDelegates;

    /** Creates a new instance of RenamePropertyRefactoringPlugin */
    public RenameTestClassRefactoringPlugin(RenameRefactoring rename) {
        this.refactoring = rename;
        treePathHandle = rename.getRefactoringSource().lookup(TreePathHandle.class);
    }

    @Override
    protected JavaSource getJavaSource(Phase phase) {
        return JavaSource.forFileObject(treePathHandle.getFileObject());
    }

    @Override
    public Problem checkParameters() {
        if (!isRenameTestClass() && !isRenameTestClassMethod()) {
            return null;
        }

        initDelegates();

        Problem p = null;
        for (RenameRefactoring delegate : renameDelegates) {
            p = JavaPluginUtils.chainProblems(p, delegate.checkParameters());
            if (p != null && p.isFatal()) {
                return p;
            }
        }
        return JavaPluginUtils.chainProblems(p, super.checkParameters());
    }

    @Override
    public Problem fastCheckParameters() {
        if (!isRenameTestClass() && !isRenameTestClassMethod()) {
            return null;
        }
        initDelegates();

        Problem p = null;
        for (RenameRefactoring delegate : renameDelegates) {
            FileObject delegateFile = delegate.getRefactoringSource().lookup(FileObject.class);
	    if(!isRenameTestClassMethod()) {
		delegate.setNewName(newName(treePathHandle.getFileObject(), delegateFile, refactoring.getNewName()));
	    }
            p = JavaPluginUtils.chainProblems(p, delegate.fastCheckParameters());
            if (p != null && p.isFatal()) {
                return p;
            }
        }
        return JavaPluginUtils.chainProblems(p, super.fastCheckParameters());
    }

    @Override
    protected Problem preCheck(CompilationController javac) throws IOException {
        if (!isRenameTestClass() && !isRenameTestClassMethod()) {
            return null;
        }
        initDelegates();
        Problem p = null;
        for (RenameRefactoring delegate : renameDelegates) {
            p = JavaPluginUtils.chainProblems(p, delegate.preCheck());
            if (p != null && p.isFatal()) {
                return p;
            }
        }
        return JavaPluginUtils.chainProblems(p, super.preCheck(javac));
    }

    @Override
    public Problem prepare(RefactoringElementsBag reb) {
        if (!isRenameTestClass() && !isRenameTestClassMethod()) {
            return null;
        }
        initDelegates();
        fireProgressListenerStart(ProgressEvent.START, renameDelegates.length);
        Problem p = null;
        for (RenameRefactoring delegate : renameDelegates) {
            p = JavaPluginUtils.chainProblems(p, delegate.prepare(reb.getSession()));
            if (p != null && p.isFatal()) {
                return p;
            }
            fireProgressListenerStep();
        }
        fireProgressListenerStop();
        return p;
    }

    private boolean isRenameTestClass() {
        JavaRenameProperties renameProps = refactoring.getContext().lookup(JavaRenameProperties.class);
        if (renameProps != null && renameProps.isIsRenameTestClass()) {
            return true;
        }
        return false;
    }

    private boolean isRenameTestClassMethod() {
        JavaRenameProperties renameProps = refactoring.getContext().lookup(JavaRenameProperties.class);
        if (renameProps != null && renameProps.isIsRenameTestClassMethod()) {
            return true;
        }
        return false;
    }

    private boolean inited = false;

    private void initDelegates() {
        if (inited) {
            return;
        }
        
        final List<RenameRefactoring> renameRefactoringsList = Collections.synchronizedList(new LinkedList<RenameRefactoring>());
        final ElementKind elementKind = treePathHandle.getElementHandle().getKind();

        if(SUPPORTED.contains(elementKind)) {
            final FileObject fileObject = treePathHandle.getFileObject();
            Collection<? extends TestLocator> testLocators = Lookup.getDefault().lookupAll(TestLocator.class);
            for (final TestLocator testLocator : testLocators) {
                if(testLocator.appliesTo(fileObject)) {
                    if(testLocator.asynchronous()) {
                        CountDownLatch latch = new CountDownLatch(1);
                        testLocator.findOpposite(fileObject, -1, new TestLocator.LocationListener() {
                            @Override
                            public void foundLocation(FileObject fo, LocationResult location) {
                                try {
                                    if(elementKind == ElementKind.CLASS) {
                                        addIfMatch(location, testLocator, fo, renameRefactoringsList);
                                    } else if(elementKind == ElementKind.METHOD) {
                                        addIfMatchMethod(location, testLocator, renameRefactoringsList);
                                    }
                                } finally {
                                    latch.countDown();
                                }
                            }
                        });
                        try {
                            latch.await(10000000000L, TimeUnit.NANOSECONDS);
                        } catch (InterruptedException ex) {
                            Logger.getLogger(RenamePropertyRefactoringPlugin.class.getName())
                                    .fine("Finding test class took too long, or it was interupted"); //NOI18N
                        }
                    } else {
                        LocationResult location = testLocator.findOpposite(fileObject, -1);
                        if (elementKind == ElementKind.METHOD) {
                            addIfMatchMethod(location, testLocator, renameRefactoringsList);
                        } else {
                            addIfMatch(location, testLocator, fileObject, renameRefactoringsList);
                        }
                    }
                }
            }
        }
        
        renameDelegates = renameRefactoringsList.toArray(new RenameRefactoring[0]);
        inited = true;
    }
    
    private static String newName(FileObject testedFile, FileObject testFile, String newName) {
        String testedName = testedFile.getName();
        String testName = testFile.getName();
        
        return testName.replace(testedName, newName);
    }
    
    private void addIfMatch(LocationResult location, final TestLocator testLocator, final FileObject fileObject, final List<RenameRefactoring> renameRefactoringsList) {
        if(location.getFileObject() != null && testLocator.getFileType(location.getFileObject()).equals(TestLocator.FileType.TEST)) {
            RenameRefactoring renameRefactoring = new RenameRefactoring(Lookups.singleton(location.getFileObject()));
            renameRefactoring.setNewName(newName(fileObject, location.getFileObject(), refactoring.getNewName()));
            renameRefactoring.setSearchInComments(true);
            renameRefactoringsList.add(renameRefactoring);
        }
    }

    private void addIfMatchMethod(final LocationResult location, final TestLocator testLocator, final List<RenameRefactoring> renameRefactoringsList) {
        if(location.getFileObject() != null && testLocator.getFileType(location.getFileObject()).equals(TestLocator.FileType.TEST)) {
	    try {
		JavaSource.forFileObject(location.getFileObject()).runUserActionTask(new Task<CompilationController>() {
		    @Override
		    public void run(CompilationController javac) throws Exception {
			final Element methodElement = treePathHandle.resolveElement(javac);
			String methodName = methodElement.getSimpleName().toString();
			String testMethodName = RefactoringUtils.getTestMethodName(methodName);
			javac.toPhase(JavaSource.Phase.RESOLVED);
			CompilationUnitTree cut = javac.getCompilationUnit();
			Tree classTree = cut.getTypeDecls().get(0);
			List<? extends Tree> members = ((ClassTree) classTree).getMembers();
			for (int i = 0; i < members.size(); i++) {
                            Tree member = members.get(i);
                            if(member.getKind() != Tree.Kind.METHOD) {
                                continue;
                            }
                            MethodTree methodTree = (MethodTree) member;
			    if (methodTree.getName().contentEquals(testMethodName)
                                    && methodTree.getReturnType().getKind() == Tree.Kind.PRIMITIVE_TYPE
                                    && ((PrimitiveTypeTree) methodTree.getReturnType()).getPrimitiveTypeKind() == TypeKind.VOID) {
                                 // test method should at least be void
                                classTree = ((ClassTree) classTree).getMembers().get(i);
                                TreePath tp = TreePath.getPath(cut, classTree);
                                RenameRefactoring renameRefactoring = new RenameRefactoring(Lookups.singleton(TreePathHandle.create(tp, javac)));
                                renameRefactoring.setNewName(RefactoringUtils.getTestMethodName(refactoring.getNewName()));
                                renameRefactoring.setSearchInComments(true);
                                renameRefactoringsList.add(renameRefactoring);
                                break;
                            }
			}
		    }
		}, true);
	    } catch (IOException ex) {
		Exceptions.printStackTrace(ex);
	    }
	}
    }
}
