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

import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import org.netbeans.api.fileinfo.NonRecursiveFolder;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.TreePathHandle;
import org.netbeans.modules.refactoring.api.AbstractRefactoring;
import org.netbeans.modules.refactoring.api.MoveRefactoring;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.api.RenameRefactoring;
import org.netbeans.modules.refactoring.api.SafeDeleteRefactoring;
import org.netbeans.modules.refactoring.api.SingleCopyRefactoring;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.netbeans.modules.refactoring.spi.RefactoringPlugin;
import org.netbeans.modules.refactoring.spi.RefactoringPluginFactory;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;

/**
 * Entry point for refactoring, registered in META-INF/services. Whenever
 * a refactoring is about to start, createInstance is called where we analyze
 * the type of refactoring and attach RefactoringInfo object to the refactoring,
 * which can be later accessed from various places where we prepare or perform
 * the additional changes in forms and resources.
 * 
 *  @author Tomas Pavek
 */
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.refactoring.spi.RefactoringPluginFactory.class)
public class RefactoringPluginFactoryImpl implements RefactoringPluginFactory {

    @Override
    public RefactoringPlugin createInstance(AbstractRefactoring refactoring) {
        RefactoringInfo.ChangeType changeType = null;
        List<FileObject> fileList = new LinkedList<FileObject>();
        List<String> nameList = new LinkedList<String>();

        // We must do some more analysis here, though it would be better to do
        // it later in the plugin's prepare method, but we can't be sure the
        // guarded handler is not called sooner from java plugin than our plugin.

        if (refactoring instanceof RenameRefactoring) {
            FileObject primaryFile = null;
            String oldName = null;
            Lookup sourceLookup = refactoring.getRefactoringSource();
            FileObject file = sourceLookup.lookup(FileObject.class);
            NonRecursiveFolder pkgFolder = sourceLookup.lookup(NonRecursiveFolder.class);
            final TreePathHandle tpHandle = sourceLookup.lookup(TreePathHandle.class);
            // assumption: if file is being renamed (even as result of renaming
            // a class) then file != null, and if something inside the class
            // is renamed then file == null

            if (file != null && RefactoringInfo.isJavaFile(file)) {
                // renaming a java file within the same package
                // (can be a form, or a component used in a form, or both)
                 if (isOnSourceClasspath(file)) {
                    changeType = RefactoringInfo.ChangeType.CLASS_RENAME;
                    primaryFile = file;
                    oldName = file.getName();
                 }
            } else if (file == null && tpHandle != null) {
                // renaming an element inside a java file
                primaryFile = tpHandle.getFileObject();
                if (RefactoringInfo.isJavaFileOfForm(primaryFile)) {
                    JavaSource source = JavaSource.forFileObject(tpHandle.getFileObject());
                    final RefactoringInfo.ChangeType[] changeTypes = new RefactoringInfo.ChangeType[1];
                    final String[] oldNames = new String[1];
                    try {
                        source.runUserActionTask(new CancellableTask<CompilationController>() {
                            @Override
                            public void cancel() {
                            }
                            @Override
                            public void run(CompilationController controller) throws Exception {
                                controller.toPhase(JavaSource.Phase.RESOLVED);
                                Element el = tpHandle.resolveElement(controller);
                                if (el != null) {
                                    switch(el.getKind()) {
                                    case FIELD:
                                        changeTypes[0] = RefactoringInfo.ChangeType.VARIABLE_RENAME;
                                        break;
                                    case LOCAL_VARIABLE:
                                        Element parentEl = el.getEnclosingElement();
                                        if (parentEl.getKind() == ElementKind.METHOD
                                                && "initComponents".equals(parentEl.getSimpleName().toString()) // NOI18N
                                                && ((ExecutableElement)parentEl).getParameters().isEmpty()) {
                                            changeTypes[0] = RefactoringInfo.ChangeType.VARIABLE_RENAME;
                                        }
                                        break;
                                    }
                                    // [should we also check if it really matches an existing component in the form?]
                                    oldNames[0] = el.getSimpleName().toString();
                                }
                            }
                        }, true);
                    } catch (IOException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                    changeType = changeTypes[0];
                    oldName = oldNames[0];
                }
                // TBD: changing a property method of a component
            } else if (file != null && file.isFolder()) {
                // renaming a folder (incl. subfolders)
                if (isOnSourceClasspath(file)) {
                    changeType = RefactoringInfo.ChangeType.FOLDER_RENAME;
                    primaryFile = file;
                    oldName = file.getName();
                }
            } else if (pkgFolder != null) {
                // renaming a package (without subfolders)
                if (isOnSourceClasspath(pkgFolder.getFolder())) {
                    changeType = RefactoringInfo.ChangeType.PACKAGE_RENAME;
                    primaryFile = pkgFolder.getFolder();
                    ClassPath cp = ClassPath.getClassPath(primaryFile, ClassPath.SOURCE);
                    oldName = cp.getResourceName(primaryFile, '.', false);
                }
            }
            if (changeType != null) {
                fileList.add(primaryFile);
                nameList.add(oldName);
            }
        } else {
            if (refactoring instanceof MoveRefactoring) {
                Collection<? extends FileObject> files = refactoring.getRefactoringSource().lookupAll(FileObject.class);
                for (FileObject file : files) {
                    if (RefactoringInfo.isJavaFile(file) && isOnSourceClasspath(file)) {
                        // moving a java file (between packages)
                        changeType = RefactoringInfo.ChangeType.CLASS_MOVE;
                        fileList.add(file);
                        ClassPath cp = ClassPath.getClassPath(file, ClassPath.SOURCE);
                        nameList.add(cp.getResourceName(file, '.', false));
                    }
                }
            } else if (refactoring instanceof SingleCopyRefactoring) {
                Collection<? extends FileObject> files = refactoring.getRefactoringSource().lookupAll(FileObject.class);
                for (FileObject file : files) {
                    if (RefactoringInfo.isJavaFileOfForm(file) && isOnSourceClasspath(file)) {
                        // copying a java file
                        changeType = RefactoringInfo.ChangeType.CLASS_COPY;
                        fileList.add(file);
                        nameList.add(file.getName());
                    }
                }
            } else if (refactoring instanceof SafeDeleteRefactoring) {
                Collection<? extends FileObject> files = refactoring.getRefactoringSource().lookupAll(FileObject.class);
                for (FileObject file : files) {
                    if (file != null && RefactoringInfo.isJavaFileOfForm(file) && isOnSourceClasspath(file)) {
                        // deleting a form
                        changeType = RefactoringInfo.ChangeType.CLASS_DELETE;
                        fileList.add(file);
                        nameList.add(null);
                    }
                }
            }
        }

        if (changeType != null) {
            FileObject[] originalFiles = fileList.toArray(new FileObject[fileList.size()]);
            String[] oldNames = nameList.toArray(new String[nameList.size()]);
            RefactoringInfo refInfo = new RefactoringInfo(refactoring, changeType, originalFiles, oldNames);
            refactoring.getContext().add(refInfo); // to be accessible to the GuardedBlockHandlerFactoryImpl
            return new RefactoringPluginImpl(refInfo);
        }
        return null;
    }

    private static boolean isOnSourceClasspath(FileObject fo) {
        // TBD
        return true;
    }

    // -----

    private static class RefactoringPluginImpl implements RefactoringPlugin {

        private RefactoringInfo refInfo;

        RefactoringPluginImpl(RefactoringInfo refInfo) {
            this.refInfo = refInfo;
        }

        @Override
        public Problem preCheck() {
            return null;
        }

        @Override
        public Problem checkParameters() {
            return null;
        }

        @Override
        public Problem fastCheckParameters() {
            return null;
        }

        @Override
        public void cancelRequest() {
        }

        @Override
        public Problem prepare(RefactoringElementsBag refactoringElements) {
            // even if guarded blocks are not affected directly we might want some changes
            for (FileObject file : refInfo.getOriginalFiles()) {
                if (RefactoringInfo.isJavaFileOfForm(file)) {
                    FormRefactoringUpdate update = refInfo.getUpdateForFile(file);
                    switch (refInfo.getChangeType()) {
                    case CLASS_DELETE: // in case of delete we only backup the form file
                        refactoringElements.registerTransaction(update);
                        return null;
                    case CLASS_RENAME: // renaming form class, always needs to load - auto-i18n
                        if (!update.prepareForm(true)) {
                            return new Problem(true, "Error loading form. Cannot update generated code.");
                        }
                        break;
                    // for VARIABLE_RENAME and EVENT_HANDLER_RENAME we don't know yet
                    // if they affect the form - guarded block handler will take care
                    }
                    refactoringElements.add(refInfo.getRefactoring(), update.getPreviewElement());
                    refactoringElements.addFileChange(refInfo.getRefactoring(), update);
                } else if (refInfo.getChangeType() == RefactoringInfo.ChangeType.PACKAGE_RENAME
                           || refInfo.getChangeType() == RefactoringInfo.ChangeType.FOLDER_RENAME) {
                    boolean anyForm = false;
                    for (FileObject fo : file.getChildren()) {
                        if (RefactoringInfo.isJavaFileOfForm(fo)) {
                            anyForm = true;
                            FormRefactoringUpdate update = refInfo.getUpdateForFile(fo);
                            refactoringElements.addFileChange(refInfo.getRefactoring(), update);
                        }
                    }
                    if (anyForm) {
                        // TODO add refactoring element informing about updating references to resources in GUI forms in this package
                    }
                }
            }
            return null;
        }
    }
}
