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

import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.modules.gsf.testrunner.api.TestCreatorProvider;
import org.netbeans.modules.gsf.testrunner.api.TestCreatorProvider.Context;
import org.netbeans.modules.gsf.testrunner.api.TestCreatorProvider.Registration;
import org.netbeans.modules.gsf.testrunner.plugin.CommonPlugin;
import org.netbeans.modules.gsf.testrunner.ui.api.UICommonUtils;
import org.netbeans.modules.java.testrunner.GuiUtils;
import org.netbeans.modules.junit.api.JUnitTestUtil;
import org.netbeans.modules.junit.plugin.JUnitPlugin;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;

/**
 *
 * @author Theofanis Oikonomou
 */
@Registration(displayName=GuiUtils.JUNIT_TEST_FRAMEWORK, identifier = TestCreatorProvider.IDENTIFIER_JUNIT)
public class JUnitTestCreatorProvider extends TestCreatorProvider {

    @Override
    public boolean enable(FileObject[] activatedFOs) {
        if (activatedFOs == null || activatedFOs.length == 0) {
            return false;
        }
        /*
         * In most cases, there is just one node selected - that is why
         * this case is handled in a special, more effective way
         * (no collections and iterators created).
         */
        if (activatedFOs.length == 1) {
            FileObject fileObj = activatedFOs[0];
            
            Project project;
            if ((fileObj != null)
                && fileObj.isValid()
                // selected FO might be folder or java file
                && (fileObj.isFolder() || JUnitTestUtil.isJavaFile(fileObj))
                && ((project = FileOwnerQuery.getOwner(fileObj)) != null)
                && (getSourceGroup(fileObj, project) != null)) {

                JUnitPlugin plugin = JUnitTestUtil.getPluginForProject(project);
                return JUnitTestUtil.canCreateTests(plugin,
                                                                    fileObj);
            } else {
                return false;
            }
        }

        final Collection<FileObject> fileObjs
                = new ArrayList<FileObject>(activatedFOs.length);
        Project theProject = null;
        boolean result = false;
        for (FileObject fileObj : activatedFOs) {
            if ((fileObj == null) || !fileObj.isValid()
                // selected FO might be folder or java file
                && (fileObj.isFolder() || JUnitTestUtil.isJavaFile(fileObj))) {
                continue;
            }

            fileObjs.add(fileObj);

            Project prj = FileOwnerQuery.getOwner(fileObj);
            if (prj != null) {
                if (theProject == null) {
                    theProject = prj;
                }
                if (prj != theProject) {
                    return false;        /* files from different projects */

                }

                if (getSourceGroup(fileObj, prj) != null) {
                    result = true;
                }
            }
        }

        if (theProject != null) {
            JUnitPlugin plugin = JUnitTestUtil.getPluginForProject(theProject);
            result &= JUnitTestUtil.canCreateTests(
                            plugin,
                            fileObjs.toArray(new FileObject[fileObjs.size()]));
        }

        return result;
    }

    @Override
    public void createTests(Context context) {
        String problem;
        if ((problem = checkNodesValidity(context.getActivatedFOs())) != null) {
            // TODO report problem
            NotifyDescriptor msg = new NotifyDescriptor.Message(
                    problem, NotifyDescriptor.WARNING_MESSAGE);
            DialogDisplayer.getDefault().notify(msg);
            return;
        }

        final FileObject[] filesToTest = context.getActivatedFOs();
        if (filesToTest == null) {
            return;     //XXX: display some message
        }

        /*
         * Determine the plugin to be used:
         */
        final JUnitPlugin plugin = JUnitTestUtil.getPluginForProject(
                FileOwnerQuery.getOwner(filesToTest[0]));

        if (!JUnitTestUtil.createTestActionCalled(
                plugin, filesToTest)) {
            return;
        }

        /*
         * Store the configuration data:
         */
        final boolean singleClass = context.isSingleClass();
        final Map<CommonPlugin.CreateTestParam, Object> params = JUnitTestUtil.getSettingsMap(!singleClass);
        if (singleClass) {
            params.put(CommonPlugin.CreateTestParam.CLASS_NAME, context.getTestClassName());
        }
        final FileObject targetFolder = context.getTargetFolder();
        if(context.isIntegrationTests()) {
            params.put(CommonPlugin.CreateTestParam.INC_GENERATE_INTEGRATION_TEST, Boolean.TRUE);
        } else {
            params.put(CommonPlugin.CreateTestParam.INC_GENERATE_INTEGRATION_TEST, Boolean.FALSE);
        }

        RequestProcessor.getDefault().post(new Runnable() {

            @Override
            public void run() {
                /*
                 * Now create the tests:
                 */
                final FileObject[] testFileObjects = JUnitTestUtil.createTests(
                        plugin,
                        filesToTest,
                        targetFolder,
                        params);

                /*
                 * Open the created/updated test class if appropriate:
                 */
                if (testFileObjects.length == 1) {
                    try {
                        DataObject dobj = DataObject.find(testFileObjects[0]);
                        final EditorCookie ec = dobj.getLookup().lookup(EditorCookie.class);
                        if (ec != null) {
                            EventQueue.invokeLater(new Runnable() {

                                @Override
                                public void run() {
                                    ec.open();
                                }
                            });
                        }
                    } catch (DataObjectNotFoundException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        });
    }

    /**
     * Checks that the selection of nodes the dialog is invoked on is valid. 
     * @return String message describing the problem found or null, if the
     *         selection is ok
     */
    private static String checkNodesValidity(FileObject[] files) {

        Project project = getProject(files);
        if (project == null) {
            return NbBundle.getMessage(JUnitTestCreatorProvider.class,
                                       "MSG_multiproject_selection");   //NOI18N
        }

        if (!checkPackages(files)) {
            return NbBundle.getMessage(JUnitTestCreatorProvider.class,
                                       "MSG_invalid_packages");         //NOI18N
        }

        return null;
    }

    /**
     * Extracts {@code FileObject}s from the given nodes.
     * Nodes that have (direct or indirect) parent nodes among the given
     * nodes are ignored.
     *
     * @return  a non-empty array of {@code FileObject}s
     *          represented by the given nodes;
     *          or {@code null} if no {@code FileObject} was found;
     */
    private static FileObject[] getFileObjectsFromNodes(final Node[] nodes){
        FileObject[] fileObjects = new FileObject[nodes.length];
        List<FileObject> fileObjectsList = null;

        for (int i = 0; i < nodes.length; i++) {
            final Node node = nodes[i];
            final FileObject fo;
            if (!hasParentAmongNodes(nodes, i)
                    && ((fo = getTestFileObject(node)) != null)) {
                if (fileObjects != null) {
                    fileObjects[i] = fo;
                } else {
                    if (fileObjectsList == null) {
                        fileObjectsList = new ArrayList<FileObject>(
                                                        nodes.length - i);
                    }
                    fileObjectsList.add(fo);
                }
            } else {
                fileObjects = null;     //signs that some FOs were skipped
            }
        }
        if (fileObjects == null) {
            if (fileObjectsList != null) {
                fileObjects = fileObjectsList.toArray(
                        new FileObject[fileObjectsList.size()]);
                fileObjectsList = null;
            }
        }

        return fileObjects;
    }

    /**
     * Check that all the files (folders or java files) have correct java
     * package names.
     * @return true if all are fine
     */
    private static boolean checkPackages(FileObject[] files) {
        if (files.length == 0) {
            return true;
        } else {
            Project project = FileOwnerQuery.getOwner(files[0]);
            for (int i = 0 ; i < files.length; i++) {
                String packageName = getPackage(project, files[i]);
                if ((packageName == null)
                        || !JUnitTestUtil.isValidPackageName(packageName)) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Get the package name of <code>file</code>.
     *
     * @param project owner of the file (for performance reasons)
     * @param file the FileObject whose packagename to get
     * @return package name of the file or null if it cannot be retrieved
     */
    private static String getPackage(Project project, FileObject file) {
        SourceGroup srcGrp = JUnitTestUtil.findSourceGroupOwner(project, file);
        if (srcGrp!= null) {
            ClassPath cp = ClassPathSupport.createClassPath(
                    new FileObject [] {srcGrp.getRootFolder()});
            return cp.getResourceName(file, '.', false);
        } else {
            return null;
        }
    }


    private static FileObject[] getFiles(Node[] nodes) {
        FileObject[] ret = new FileObject[nodes.length];
        for (int i = 0 ; i < nodes.length ; i++) {
            ret[i]  = UICommonUtils.getFileObjectFromNode(nodes[i]);
        }
        return ret;
    }

    /**
     * Get the single project for <code>nodes</code> if there is such.
     * If the nodes belong to different projects or some of the nodes doesn't
     * have a project, return null.
     */
    private static Project getProject(FileObject[] files) {
        Project project = null;
        for (int i = 0 ; i < files.length; i++) {
            Project nodeProject = FileOwnerQuery.getOwner(files[i]);
            if (project == null) {
                project = nodeProject;
            } else if (project != nodeProject) {
                return null;
            }
        }
        return project;
    }

    /**
     * Grabs and checks a <code>FileObject</code> from the given node.
     * If either the file could not be grabbed or the file does not pertain
     * to any project, a message is displayed.
     *
     * @param  node  node to get a <code>FileObject</code> from.
     * @return  the grabbed <code>FileObject</code>,
     *          or <code>null</code> in case of failure
     */
    private static FileObject getTestFileObject(final Node node) {
        final FileObject fo = UICommonUtils.getFileObjectFromNode(node);
        if (fo == null) {
            JUnitTestUtil.notifyUser(NbBundle.getMessage(
                    JUnitTestCreatorProvider.class,
                    "MSG_file_from_node_failed"));                      //NOI18N
            return null;
        }
        ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE);
        if (cp == null) {
            JUnitTestUtil.notifyUser(NbBundle.getMessage(
                    JUnitTestCreatorProvider.class,
                    "MSG_no_project",                                   //NOI18N
                    fo));
            return null;
        }
        return fo;
    }

    private static boolean hasParentAmongNodes(final Node[] nodes,
                                               final int idx) {
        Node node;

        node = nodes[idx].getParentNode();
        while (null != node) {
            for (int i = 0; i < nodes.length; i++) {
                if (i == idx) {
                    continue;
                }
                if (node == nodes[i]) {
                    return true;
                }
            }
            node = node.getParentNode();
        }
        return false;
    }
    
}
