/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.module.tools;

import java.io.File;
import java.io.IOException;
import java.module.Module;
import java.module.ModuleArchiveInfo;
import java.module.ModuleDefinition;
import java.module.ModuleInitializationException;
import java.module.Modules;
import java.module.Query;
import java.module.Repository;
import java.module.VersionConstraint;
import java.util.LinkedHashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import sun.module.JamUtils;

/**
 * Provides a means to visit each of a module's imports.
 * @since 1.7
 */
public class ImportTraverser implements Iterable<Module> {
    private Set<Module> visited = new LinkedHashSet<Module>();

    /**
     * Invokes a visitor on each of the imports of the module identified by
     * the parameters.   The imports are visited in depth-first order with respect to
     * the graph of interconnected modules.  Note that the module identified
     * by parameters is the first one visited.
     *
     * @param repo repository in which to find a named module
     * @param name name of the module to find
     * @param constraint version constraint on the module; may be null
     * @throws ModuleInitializationException if the identified module cannot be
     * instantiated
     */
    public void visit(Repository repo,
                      String name,
                      VersionConstraint constraint)
            throws ModuleInitializationException{

        visit(repo, name, constraint, null, null);
    }

    /**
     * Invokes a visitor on each of the imports of the module identified by
     * the parameters.   The imports are visited in depth-first order with respect to
     * the graph of interconnected modules.  Note that the module identified
     * by parameters is the first one visited.
     *
     * @param jamName name of a JAM file for which dependencies are returned
     * @throws java.io.IOException if a Repository for the named JAM file cannot
     * be created
     * @throws ModuleInitializationException if the identified module cannot be
     * instantiated
     */
    public void visit(String jamName)
            throws IOException, ModuleInitializationException {

        File repoDir = JamUtils.createTempDir();
        Repository repo = Modules.newLocalRepository("importvisitor", repoDir);
        ModuleArchiveInfo mai = repo.install(new File(jamName).toURI().toURL());
        
        visit(repo, mai.getName(), mai.getVersion().toVersionConstraint(),
              mai.getPlatform(), mai.getArch());
    }

    /**
     * Invokes a visitor on each of the imports of the module identified by
     * the parameters.   The imports are visited in depth-first order with respect to
     * the graph of interconnected modules.  Note that the module identified
     * by parameters is the first one visited.
     *
     * @param repo repository in which to find a named module
     * @param name name of the module to find
     * @param constraint version constraint on the module; may be null
     * @param platform platform of the module to find; may be null
     * @param arch architecture of the module to find; may be null
     * @throws ModuleInitializationException if the identified module cannot be
     * instantiated
     * @throws IllegalArgumentException if one of {@code platform, arch} is
     * null and the other is not, or if the {@code repository} finds more than
     * one module matching the given parameters.
     */
    public void visit(Repository repo,
                         String name, VersionConstraint constraint,
                         String platform, String arch)
            throws ModuleInitializationException {

        if (repo == null || name == null) {
            throw new NullPointerException();
        }

        if (platform == null ^ arch == null) {
            throw new IllegalArgumentException(
                "module platform and arch must be either both null or both non-null");
        }
        
        Query q = Query.module(
            name, constraint == null ? VersionConstraint.DEFAULT : constraint);
        if (platform != null) {
            q = Query.and(
                q, Query.annotation(java.module.annotation.PlatformBinding.class));
        }

        List<ModuleDefinition> mdList = repo.find(q);
        if (mdList.size() > 1) {
            throw new IllegalArgumentException(
                "more than one matching module found for name=" + name
                + " version=" + constraint
                + " platform=" + platform + " arch=" + arch);
        } else if (mdList.size() == 1) {
            Module m = mdList.get(0).getModuleInstance();
            init(m);
            visit(repo, m);
        }
    }

    public void visit(Repository repo,
                       Module module) {
                       
        if (visited.contains(module)) {
            return;
        }
        visited.add(module);
        if (preVisit(module)) {
            List<Module> imported = module.getImportedModules();
            for (Module m : imported) {
                visit(m);
                visit(repo, m);
            }
        }
        postVisit(module);
    }

    /**
     * @return an unmodifiable list of the {@code Module} instances which have
     * been visited; note that the list is ordered by the depth-first
     * traversal of module imports.
     */
    public Iterator<Module> iterator() {
        return visited.iterator();
    }

    /**
     * @param m {@code Module} possibly visited by the traverser
     * @return true if the given {@code Module} has been visited, false
     * otherwise
     */
    protected boolean visited(Module m) {
        return visited.contains(m);
    }

    /**
     * Invoked with the {@code Module} identified by the client-called {@code
     * visit} method before any other {@code Module} instances are visited.
     * @param m First {@code Module} visited by the traverser
     */
    protected void init(Module m) { }

    /**
     * Invoked on a {@code Module} instance before its imports are visited.
     * @param m {@code Module} which imports the module about to be visited by
     * {@link #visit(Module)}
     * @return true if the {@code Module} should be visited, false otherwise.
     */
    protected boolean preVisit(Module m) {
        return true;
    }

    /**
     * Invoked on a {@code Module} instance as it is visited
     * @param m {@code Module} being visited
     */
    protected void visit(Module m) { }

    /**
     * Invoked on a {@code Module} instance after its imports are visited.
     * @param m {@code Module} which imports the module that was just visited by
     * {@link #visit(Module)}
     */
    protected void postVisit(Module m) { }
}
