/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 * Contributor(s):
 *
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
 */

package org.netbeans.modules.maven.classpath;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.project.MavenProject;
import org.netbeans.api.annotations.common.SuppressWarnings;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.JavaClassPathConstants;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.project.ant.AntArtifact;
import org.netbeans.api.project.libraries.Library;
import org.netbeans.modules.maven.api.ModelUtils;
import org.netbeans.modules.maven.api.ModelUtils.Descriptor;
import org.netbeans.modules.maven.api.NbMavenProject;
import org.netbeans.modules.maven.indexer.api.NBVersionInfo;
import org.netbeans.modules.maven.indexer.api.RepositoryPreferences;
import org.netbeans.modules.maven.indexer.api.RepositoryQueries;
import org.netbeans.modules.maven.model.ModelOperation;
import org.netbeans.modules.maven.model.pom.Dependency;
import org.netbeans.modules.maven.model.pom.POMModel;
import org.netbeans.modules.maven.model.pom.Repository;
import org.netbeans.spi.java.project.classpath.ProjectClassPathModifierImplementation;
import org.netbeans.spi.project.ProjectServiceProvider;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;

/**
 * an implementation of ProjectClassPathModifierImplementation that tried to match 
 * maven dependencies to the way classpath items are added through this api.
 * @author mkleint
 */
@ProjectServiceProvider(service=ProjectClassPathModifierImplementation.class, projectType="org-netbeans-modules-maven")
public class CPExtender extends ProjectClassPathModifierImplementation {

    private static final Logger LOG = Logger.getLogger(CPExtender.class.getName());

    private Project project;
    private static final String POM_XML = "pom.xml"; //NOI18N
    
    public CPExtender(Project project) {
        this.project = project;
    }
    
    private boolean addRemoveLibrary(Library library, POMModel model, String scope, boolean add) throws IOException {
        Boolean modified = checkLibraryForPoms(library, model, scope, add);
        if (modified == null) {
            List<URL> urls = library.getContent("classpath"); //NOI18N
            modified = urls.size() > 0;
            assert model != null;
            for (URL url : urls) {
                FileObject fo = URLMapper.findFileObject(url);
                if (fo == null) {
                    throw new IOException("Could find no file corresponding to " + url);
                }
                FileObject jarFO = FileUtil.getArchiveFile(fo);
                if (jarFO == null || FileUtil.getArchiveRoot(jarFO) != fo) {
                    throw new IOException("Cannot add non-root of JAR: " + url);
                }
                File jar = FileUtil.toFile(jarFO);
                if (jar == null) {
                    throw new IOException("no disk file found corresponding to " + jarFO);
                }
                if (jar.isDirectory()) {
                    throw new IOException("Cannot add folders to Maven projects as dependencies: " + url); //NOI18N
                }
                modified = modified && addRemoveJAR(jar, model, scope, add);
            }
        }
        return modified;
    }
    
    private boolean addRemoveJAR(File jar, POMModel mdl, String scope, boolean add) throws IOException {
        if (!add) {
            throw new UnsupportedOperationException("removing JARs not yet supported");
        }
        NBVersionInfo dep = null;
        for (NBVersionInfo _dep : RepositoryQueries.findBySHA1Result(jar, RepositoryPreferences.getInstance().getRepositoryInfos()).getResults()) {
            if (!"unknown.binary".equals(_dep.getGroupId())) {
                dep = _dep;
                break;
            }
        }
        if (dep == null) {
            dep = new NBVersionInfo(null, "unknown.binary", jar.getName().replaceFirst("[.]jar$", ""), "SNAPSHOT", null, null, null, null, null);
            addJarToPrivateRepo(jar, mdl, dep);
        }
        //if not found anywhere, add to a custom file:// based repository structure within the project's directory.
        boolean added = false;
        Dependency dependency = ModelUtils.checkModelDependency(mdl, dep.getGroupId(), dep.getArtifactId(), false);
        if (dependency == null) {
            dependency = ModelUtils.checkModelDependency(mdl, dep.getGroupId(), dep.getArtifactId(), true);
            LOG.log(Level.FINE, "added new dep {0} as {1}", new Object[] {jar, dep});
            added = true;
        }
        if (!Utilities.compareObjects(dep.getVersion(), dependency.getVersion())) {
            dependency.setVersion(dep.getVersion());
            LOG.log(Level.FINE, "upgraded version on {0} as {1}", new Object[] {jar, dep});
            added = true;
        }
        if (!Utilities.compareObjects(scope, dependency.getScope())) {
            dependency.setScope(scope);
            LOG.log(Level.FINE, "changed scope on {0} as {1}", new Object[] {jar, dep});
            added = true;
        }
        return added;
    }
    
    /**
     * @return true if something was added, false if everything was already there, null if could not do anything
     */
    @SuppressWarnings("NP_BOOLEAN_RETURN_NULL")
    private Boolean checkLibraryForPoms(Library library, POMModel model, String scope, boolean add) {
        if (!"j2se".equals(library.getType())) {//NOI18N
            //only j2se library supported for now..
            return null;
        }
        Boolean modified = null;
        Descriptor d = ModelUtils.checkLibraries(library);
        for (ModelUtils.LibraryDescriptor result : d.getDependencies()) {
            LOG.log(Level.FINE, "found {0} for {1}", new Object[] {result, library.getName()});
                //set dependency
                modified = false;
                Dependency dep = ModelUtils.checkModelDependency(model, result.getGroupId(), result.getArtifactId(), false);
                if (!add) {
                    if (dep != null &&
                            Utilities.compareObjects(result.getVersion(), dep.getVersion()) &&
                            Utilities.compareObjects(scope, dep.getScope()) &&
                            Utilities.compareObjects(result.getClassifier(), dep.getClassifier())) {
                        model.removeChildComponent(dep);
                        modified = true;
                    }
                    continue;
                }
                if (dep == null) {
                    dep = ModelUtils.checkModelDependency(model, result.getGroupId(), result.getArtifactId(), true);
                    modified = true;
                }
                if (!Utilities.compareObjects(result.getVersion(), dep.getVersion())) {
                    dep.setVersion(result.getVersion());
                    modified = true;
                }
                if (!Utilities.compareObjects(result.getType(), dep.getType()) && !Utilities.compareObjects("jar", result.getType())) {
                    dep.setType(result.getType());
                    modified = true;
                }
                if (!Utilities.compareObjects(scope, dep.getScope())) {
                    dep.setScope(scope);
                    modified = true;
                }
                if (!Utilities.compareObjects(result.getClassifier(), dep.getClassifier())) {
                    dep.setClassifier(result.getClassifier());
                    modified = true;
                }
        }
        for (ModelUtils.RepositoryDescriptor result : d.getRepositories()) {
                //set repository
                org.netbeans.modules.maven.model.pom.Repository reposit = ModelUtils.addModelRepository(
                        project.getLookup().lookup(NbMavenProject.class).getMavenProject(), model, result.getRepoRoot());
                if (reposit != null) {
                    reposit.setId(library.getName());
                    reposit.setLayout(result.getRepoType());
                    reposit.setName("Repository for library " + library.getDisplayName()); //NOI18N - content coming into the pom.xml file
                    modified = true;
                }
            
        }
        LOG.log(Level.FINE, "checkLibraryForPoms on {0} -> {1}", new Object[] {library, modified});
        return modified;
    }
        
    @Override
    public SourceGroup[] getExtensibleSourceGroups() {
        Sources s = ProjectUtils.getSources(this.project);
        assert s != null;
        List<SourceGroup> grps = new ArrayList<SourceGroup>();
        SourceGroup[] java = s.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
        if (java != null) {
            grps.addAll(Arrays.asList(java));
        }
        SourceGroup[] res = s.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES);
        if (res != null) {
            grps.addAll(Arrays.asList(res));
        }
        SourceGroup[] web = s.getSourceGroups("doc_root");
        if (web != null) {
            grps.addAll(Arrays.asList(web));
        }
        
        return grps.toArray(new SourceGroup[0]);
    }
    
    @Override
    public String[] getExtensibleClassPathTypes(SourceGroup arg0) {
        return new String[] {
            ClassPath.COMPILE,
            ClassPath.EXECUTE,
            JavaClassPathConstants.COMPILE_ONLY,
            JavaClassPathConstants.PROCESSOR_PATH
        };
    }

    public @Override boolean addLibraries(Library[] libraries, SourceGroup grp, String type) throws IOException {
        return addRemoveLibraries(libraries, grp, type, true);
    }

    public @Override boolean removeLibraries(Library[] libraries, SourceGroup grp, String type) throws IOException {
        return addRemoveLibraries(libraries, grp, type, false);
    }

    private boolean addRemoveLibraries(final Library[] libraries, SourceGroup grp, String type, final boolean add) throws IOException {
        final AtomicBoolean modified = new AtomicBoolean();
        final String scope = findScope(grp, type);
        ModelOperation<POMModel> operation = new ModelOperation<POMModel>() {
            public @Override void performOperation(POMModel model) {
                for (Library library : libraries) {
                    try {
                        modified.compareAndSet(false, addRemoveLibrary(library, model, scope, add));
                    } catch (IOException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            }
        };
        FileObject pom = project.getProjectDirectory().getFileObject(POM_XML);//NOI18N
        org.netbeans.modules.maven.model.Utilities.performPOMModelOperations(pom, Collections.singletonList(operation));
        //TODO is the manual reload necessary if pom.xml file is being saved?
//                NbMavenProject.fireMavenProjectReload(project);
        if (modified.get()) {
            project.getLookup().lookup(NbMavenProject.class).triggerDependencyDownload();
        }
        return modified.get();
    }

    public @Override boolean addRoots(final URL[] urls, SourceGroup grp, String type) throws IOException {
        final AtomicBoolean added = new AtomicBoolean();
        final String scope = findScope(grp, type);
        ModelOperation<POMModel> operation = new ModelOperation<POMModel>() {
            public @Override void performOperation(POMModel model) {
                for (URL url : urls) {
                    File jar = FileUtil.archiveOrDirForURL(url);
                    if (jar != null && jar.isFile()) {
                        try {
                            added.compareAndSet(false, addRemoveJAR(jar, model, scope, true));
                        } catch (IOException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    } else {
                        Logger.getLogger(CPExtender.class.getName()).log(Level.INFO, "Adding non-jar root to Maven projects makes no sense. ({0})", url); //NOI18N
                    }
                }
            }
        };
        FileObject pom = project.getProjectDirectory().getFileObject(POM_XML);//NOI18N
        org.netbeans.modules.maven.model.Utilities.performPOMModelOperations(pom, Collections.singletonList(operation));
        if (added.get()) {
            project.getLookup().lookup(NbMavenProject.class).triggerDependencyDownload();
        }
        return added.get();
    }

    @Override
    protected boolean addProjects(final Project[] projects, SourceGroup sg, String classPathType) throws IOException, UnsupportedOperationException {
        final AtomicBoolean added = new AtomicBoolean();
        final AtomicBoolean nonMavenError = new AtomicBoolean();
        final String scope = findScope(sg, classPathType);
        ModelOperation<POMModel> operation = new ModelOperation<POMModel>() {
            public @Override void performOperation(POMModel model) {
                Set<Artifact> arts = project.getLookup().lookup(NbMavenProject.class).getMavenProject().getArtifacts();
                for (Project prj: projects) {
                    NbMavenProject nbprj = prj.getLookup().lookup(NbMavenProject.class);
                    if (nbprj != null) {
                        MavenProject mp = nbprj.getMavenProject();
                        String id = mp.getArtifact().getId();
                        for (Artifact a : arts) {
                            if (a.getId().equals(id)) {
                                //already there..
                                continue;
                            }
                        }
                        Dependency dependency = ModelUtils.checkModelDependency(model, mp.getGroupId(), mp.getArtifactId(), true);
                        dependency.setVersion(mp.getVersion());
                        if (scope != null) {
                            dependency.setScope(scope);
                        }
                        added.set(true);
                    } else {
                        // unsupported usecase, not a maven project
                        nonMavenError.set(true);
                    }
                }
            }
        };
        FileObject pom = project.getProjectDirectory().getFileObject(POM_XML);//NOI18N
        org.netbeans.modules.maven.model.Utilities.performPOMModelOperations(pom, Collections.singletonList(operation));
        if (nonMavenError.get()) {
            //throw late to prevent the pom model to go bust eventually
            throw new UnsupportedOperationException("Attempting to add a non-Maven project dependency to a Maven project, not supported."); //NOI18N
        }

        return added.get();

    }

    @Override
    public boolean removeRoots(URL[] arg0, SourceGroup arg1, String arg2) throws IOException,
                                                                                    UnsupportedOperationException {
        throw new UnsupportedOperationException("Removing binary dependencies is not supported by Maven projects.");
    }
    
    @Override
    public boolean addAntArtifacts(AntArtifact[] arg0, URI[] arg1,
                                      SourceGroup arg2, String arg3) throws IOException, UnsupportedOperationException {
        throw new UnsupportedOperationException("Cannot add Ant based projects as subprojects to Maven projects.");//NOI18N
    }
    
    @Override
    public boolean removeAntArtifacts(AntArtifact[] arg0, URI[] arg1,
                                         SourceGroup arg2, String arg3) throws IOException, UnsupportedOperationException {
        throw new UnsupportedOperationException("Cannot remove Ant based projects as subprojects from Maven projects.");//NOI18N
    }
    
    private static String findScope(SourceGroup grp, String type) {
        String scope = ClassPath.EXECUTE.equals(type) ? Artifact.SCOPE_RUNTIME : null; //NOI18N
        //figure if we deal with test or regular sources.
        String name = grp.getName();
        if (MavenSourcesImpl.NAME_TESTSOURCE.equals(name)) {
            scope = "test"; //NOI18N
        }
        if (scope == null &&
            (JavaClassPathConstants.COMPILE_ONLY.equals(type) || JavaClassPathConstants.PROCESSOR_PATH.equals(type))) {
            scope = Artifact.SCOPE_PROVIDED;
        }
        return scope;
    }

    // XXX this is a poor solution; http://jira.codehaus.org/secure/attachment/53864/MNG-1867.zip is better
    private void addJarToPrivateRepo(File jar, POMModel mdl, NBVersionInfo dep) throws IOException {
        //first add the local repo to
        List<Repository> repos = mdl.getProject().getRepositories();
        boolean found = false;
        String path = null;
        if (repos != null) {
            for (Repository repo : repos) {
                if ("unknown-jars-temp-repo".equals(repo.getId())) { //NOI18N
                    found = true;
                    String url = repo.getUrl();
                    if (url.startsWith("file:${project.basedir}/")) { //NOI18N
                        path = url.substring("file:${project.basedir}/".length()); //NOI18N
                    } else {
                        path = "lib"; //NOI18N
                    }
                    break;
                }
            }
        }
        if (!found) {
            Repository repo = mdl.getFactory().createRepository();
            repo.setId("unknown-jars-temp-repo"); //NOI18N
            repo.setName("A temporary repository created by NetBeans for libraries and jars it could not identify. Please replace the dependencies in this repository with correct ones and delete this repository."); //NOI18N
            repo.setUrl("file:${project.basedir}/lib"); //NOI18N
            mdl.getProject().addRepository(repo);
            path = "lib"; //NOI18N
        }
        assert path != null;
        FileObject root = FileUtil.createFolder(project.getProjectDirectory(), path);
        FileObject grp = FileUtil.createFolder(root, dep.getGroupId().replace('.', '/')); //NOI18N
        FileObject art = FileUtil.createFolder(grp, dep.getArtifactId());
        FileObject ver = FileUtil.createFolder(art, dep.getVersion());
        String name = dep.getArtifactId() + '-' + dep.getVersion();
        FileObject file = FileUtil.toFileObject(jar);
        if (ver.getFileObject(name, file.getExt()) == null) { //#160803
            FileUtil.copyFile(file, ver, name, file.getExt());
        }
    }
}
