/*
 * Copyright (c) 2009, Oracle and/or its affiliates. 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.
 *
 * 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */
package com.sun.classanalyzer;

import com.sun.classanalyzer.AnnotatedDependency.OptionalDependency;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 *
 * @author Mandy Chung
 */
public class Module implements Comparable<Module> {

    private static Map<String, Module> modules = new LinkedHashMap<String, Module>();

    public static Module addModule(ModuleConfig config) {
        String name = config.module;
        if (modules.containsKey(name)) {
            throw new RuntimeException("module \"" + name + "\" already exists");
        }

        Module m = new Module(config);
        modules.put(name, m);
        return m;
    }

    public static Module findModule(String name) {
        return modules.get(name);
    }

    static Collection<Module> getAllModules() {
        return Collections.unmodifiableCollection(modules.values());
    }
    private final String name;
    private final ModuleConfig config;
    private final Set<Klass> classes;
    private final Set<ResourceFile> resources;
    private final Set<Reference> unresolved;
    private final Set<Dependency> dependents;
    private final Map<String, PackageInfo> packages;
    private final Set<Module> members;
    private Module group;
    private boolean isBaseModule;

    private Module(ModuleConfig config) {
        this.name = config.module;
        this.isBaseModule = config.isBase;
        this.classes = new TreeSet<Klass>();
        this.resources = new TreeSet<ResourceFile>();
        this.config = config;
        this.unresolved = new HashSet<Reference>();
        this.dependents = new TreeSet<Dependency>();
        this.packages = new TreeMap<String, PackageInfo>();
        this.members = new TreeSet<Module>();
        this.group = this; // initialize to itself
    }

    String name() {
        return name;
    }

    Module group() {
        return group;
    }

    boolean isBase() {
        return isBaseModule;
    }

    Set<Module> members() {
        return members;
    }

    boolean contains(Klass k) {
        return k != null && classes.contains(k);
    }

    boolean isEmpty() {
        return classes.isEmpty() && resources.isEmpty();
    }

    /**
     * Returns an Iterable of Dependency, only one for each dependent
     * module of the strongest dependency (i.e.
     * hard static > hard dynamic > optional static > optional dynamic
     */
    Iterable<Dependency> dependents() {
        Map<Module, Dependency> deps = new LinkedHashMap<Module, Dependency>();
        for (Dependency dep : dependents) {
            Dependency d = deps.get(dep.module);
            if (d == null || dep.compareTo(d) > 0) {
                deps.put(dep.module, dep);
            }
        }
        return deps.values();
    }

    @Override
    public int compareTo(Module o) {
        if (o == null) {
            return -1;
        }
        return name.compareTo(o.name);
    }

    @Override
    public String toString() {
        return name;
    }

    void addKlass(Klass k) {
        classes.add(k);
        k.setModule(this);

        // update package statistics
        String pkg = k.getPackageName();
        PackageInfo pkginfo = packages.get(pkg);
        if (pkginfo == null) {
            pkginfo = new PackageInfo(pkg);
            packages.put(pkg, pkginfo);
        }
        if (k.exists()) {
            // only count the class that is parsed
            pkginfo.add(k.getFileSize());
        }
    }

    void addResource(ResourceFile res) {
        resources.add(res);
        res.setModule(this);
    }

    void processRootsAndReferences() {
        // start with the root set
        Deque<Klass> pending = new ArrayDeque<Klass>();
        for (Klass k : Klass.getAllClasses()) {
            if (k.getModule() != null) {
                continue;
            }
            String classname = k.getClassName();
            if (config.matchesRoot(classname) && !config.isExcluded(classname)) {
                addKlass(k);
                pending.add(k);
            }
        }

        // follow all references
        Klass k;
        while ((k = pending.poll()) != null) {
            if (!classes.contains(k)) {
                addKlass(k);
            }
            for (Klass other : k.getReferencedClasses()) {
                Module otherModule = other.getModule();
                if (otherModule != null && otherModule != this) {
                    // this module is dependent on otherModule
                    addDependency(k, other);
                    continue;
                }

                if (!classes.contains(other)) {
                    if (config.isExcluded(other.getClassName())) {
                        // reference to an excluded class
                        unresolved.add(new Reference(k, other));
                    } else {
                        pending.add(other);
                    }
                }
            }
        }

        // add other matching classes that don't require dependency analysis
        for (Klass c : Klass.getAllClasses()) {
            if (c.getModule() == null) {
                String classname = c.getClassName();
                if (config.matchesIncludes(classname) && !config.isExcluded(classname)) {
                    addKlass(c);
                    // dependencies
                    for (Klass other : c.getReferencedClasses()) {
                        Module otherModule = other.getModule();
                        if (otherModule == null) {
                            unresolved.add(new Reference(c, other));
                        } else {
                            if (otherModule != this) {
                                // this module is dependent on otherModule
                                addDependency(c, other);
                            }
                        }
                    }
                }
            }
        }


        // add other matching classes that don't require dependency analysis
        for (ResourceFile res : ResourceFile.getAllResources()) {
            if (res.getModule() == null) {
                String name = res.getName();
                if (config.matchesIncludes(name) && !config.isExcluded(name)) {
                    addResource(res);
                }
            }
        }
    }

    void addDependency(Klass from, Klass to) {
        Dependency dep = new Dependency(from, to);
        dependents.add(dep);
    }

    void fixupDependencies() {
        // update dependencies for classes that were allocated to modules after
        // this module was processed.
        for (Reference ref : unresolved) {
            Module m = ref.referree().getModule();
            if (m == null || m != this) {
                addDependency(ref.referrer, ref.referree);
            }
        }

        fixupAnnotatedDependencies();
    }

    private void fixupAnnotatedDependencies() {
        // add dependencies that this klass may depend on due to the AnnotatedDependency
        dependents.addAll(AnnotatedDependency.getDependencies(this));
    }

    boolean isModuleDependence(Klass k) {
        Module m = k.getModule();
        return m == null || (!classes.contains(k) && !m.isBase());
    }

    Module getModuleDependence(Klass k) {
        if (isModuleDependence(k)) {
            Module m = k.getModule();
            if (group == this && m != null) {
                // top-level module
                return m.group;
            } else {
                return m;
            }
        }
        return null;
    }

    <P> void visit(Set<Module> visited, Visitor<P> visitor, P p) {
        if (!visited.contains(this)) {
            visited.add(this);
            visitor.preVisit(this, p);
            for (Module m : members) {
                m.visit(visited, visitor, p);
                visitor.postVisit(this, m, p);
            }
        } else {
            throw new RuntimeException("Cycle detected: module " + this.name);
        }
    }

    void addMember(Module m) {
        // merge class list
        for (Klass k : m.classes) {
            classes.add(k);
        }

        // merge resource list
        for (ResourceFile res : m.resources) {
            resources.add(res);
        }

        // merge the package statistics
        for (PackageInfo pinfo : m.getPackageInfos()) {
            String packageName = pinfo.pkgName;
            PackageInfo pkginfo = packages.get(packageName);
            if (pkginfo == null) {
                pkginfo = new PackageInfo(packageName);
                packages.put(packageName, pkginfo);
            }
            pkginfo.add(pinfo);
        }
    }

    static void buildModuleMembers() {
        // set up module member relationship
        for (Module m : modules.values()) {
            m.group = m; // initialize to itself
            for (String name : m.config.members()) {
                Module member = modules.get(name);
                if (member == null) {
                    throw new RuntimeException("module \"" + name + "\" doesn't exist");
                }
                m.members.add(member);
            }
        }

        // set up the top-level module
        Visitor<Module> groupSetter = new Visitor<Module>() {

            public void preVisit(Module m, Module p) {
                m.group = p;
                if (p.isBaseModule) {
                    // all members are also base
                    m.isBaseModule = true;
                }
            }

            public void postVisit(Module m, Module child, Module p) {
                // nop - breadth-first search
            }
        };

        // propagate the top-level module to all its members
        for (Module p : modules.values()) {
            for (Module m : p.members) {
                if (m.group == m) {
                    m.visit(new TreeSet<Module>(), groupSetter, p);
                }
            }
        }

        Visitor<Module> mergeClassList = new Visitor<Module>() {

            public void preVisit(Module m, Module p) {
                // nop - depth-first search
            }

            public void postVisit(Module m, Module child, Module p) {
                m.addMember(child);
            }
        };

        Set<Module> visited = new TreeSet<Module>();
        for (Module m : modules.values()) {
            if (m.group() == m) {
                if (m.members().size() > 0) {
                    // merge class list from all its members
                    m.visit(visited, mergeClassList, m);
                }

                // clear the dependencies before fixup
                m.dependents.clear();

                // fixup dependencies
                for (Klass k : m.classes) {
                    for (Klass other : k.getReferencedClasses()) {
                        if (m.isModuleDependence(other)) {
                            // this module is dependent on otherModule
                            m.addDependency(k, other);
                        }
                    }
                }

                // add dependencies that this klass may depend on due to the AnnotatedDependency
                m.fixupAnnotatedDependencies();
            }
        }
    }

    class PackageInfo implements Comparable {

        final String pkgName;
        int count;
        long filesize;

        PackageInfo(String name) {
            this.pkgName = name;
            this.count = 0;
            this.filesize = 0;
        }

        void add(PackageInfo pkg) {
            this.count += pkg.count;
            this.filesize += pkg.filesize;
        }

        void add(long size) {
            count++;
            filesize += size;

        }

        @Override
        public int compareTo(Object o) {
            return pkgName.compareTo(((PackageInfo) o).pkgName);
        }
    }

    Set<PackageInfo> getPackageInfos() {
        return new TreeSet<PackageInfo>(packages.values());
    }

    void printSummaryTo(String output) throws IOException {
        PrintWriter writer = new PrintWriter(output);
        try {
            long total = 0L;
            int count = 0;
            writer.format("%10s\t%10s\t%s\n", "Bytes", "Classes", "Package name");
            for (String pkg : packages.keySet()) {
                PackageInfo info = packages.get(pkg);
                if (info.count > 0) {
                    writer.format("%10d\t%10d\t%s\n", info.filesize, info.count, pkg);
                    total += info.filesize;
                    count += info.count;
                }
            }

            writer.format("\nTotal: %d bytes (uncompressed) %d classes\n", total, count);
        } finally {
            writer.close();
        }

    }

    void printClassListTo(String output) throws IOException {
        // no file created if the module doesn't have any class nor resource
        if (isEmpty()) {
            return;
        }

        PrintWriter writer = new PrintWriter(output);
        try {
            for (Klass c : classes) {
                if (c.exists()) {
                    writer.format("%s\n", c.getClassFilePathname());
                } else {
                    trace("%s in module %s missing\n", c, this);
                }
            }

        } finally {
            writer.close();
        }
    }

    void printResourceListTo(String output) throws IOException {
        // no file created if the module doesn't have any resource file
        if (resources.isEmpty()) {
            return;
        }

        PrintWriter writer = new PrintWriter(output);
        try {
            for (ResourceFile res : resources) {
                writer.format("%s\n", res.getPathname());
            }
        } finally {
            writer.close();
        }
    }

    void printDependenciesTo(String output, boolean showDynamic) throws IOException {
        // no file created if the module doesn't have any class
        if (isEmpty()) {
            return;
        }

        PrintWriter writer = new PrintWriter(output);
        try {
            // classes that this klass may depend on due to the AnnotatedDependency
            Map<Reference, Set<AnnotatedDependency>> annotatedDeps = AnnotatedDependency.getReferences(this);

            for (Klass klass : classes) {
                Set<Klass> references = klass.getReferencedClasses();
                for (Klass other : references) {
                    String classname = klass.getClassName();
                    boolean optional = OptionalDependency.isOptional(klass, other);
                    if (optional) {
                        classname = "[optional] " + classname;
                    }

                    Module m = getModuleDependence(other);
                    if (m != null || other.getModule() == null) {
                        writer.format("%-40s -> %s (%s)", classname, other, m);
                        Reference ref = new Reference(klass, other);
                        if (annotatedDeps.containsKey(ref)) {
                            for (AnnotatedDependency ad : annotatedDeps.get(ref)) {
                                writer.format(" %s", ad.getTag());
                            }
                            // printed; so remove the dependency from the annotated deps list
                            annotatedDeps.remove(ref);
                        }
                        writer.format("\n");
                    }
                }
            }


            // print remaining dependencies specified in AnnotatedDependency list
            if (annotatedDeps.size() > 0) {
                for (Map.Entry<Reference, Set<AnnotatedDependency>> entry : annotatedDeps.entrySet()) {
                    Reference ref = entry.getKey();
                    Module m = getModuleDependence(ref.referree);
                    if (m != null || ref.referree.getModule() == null) {
                        String classname = ref.referrer.getClassName();
                        boolean optional = true;
                        boolean dynamic = true;
                        String tag = "";
                        for (AnnotatedDependency ad : entry.getValue()) {
                            if (optional && !ad.isOptional()) {
                                optional = false;
                                tag = ad.getTag();
                            }
                            if (!ad.isDynamic()) {
                                dynamic = false;
                            }
                        }
                        if (!showDynamic && optional && dynamic) {
                            continue;
                        }
                        if (optional) {
                            if (dynamic) {
                                classname = "[dynamic] " + classname;
                            } else {
                                classname = "[optional] " + classname;
                            }
                        }
                        writer.format("%-40s -> %s (%s) %s%n", classname, ref.referree, m, tag);
                    }
                }
            }

        } finally {
            writer.close();
        }
    }

    static class Dependency implements Comparable<Dependency> {

        final Module module;
        final boolean optional;
        final boolean dynamic;

        Dependency(Klass from, Klass to) {
            // static dependency
            this.module = to.getModule() != null ? to.getModule().group() : null;
            this.optional = OptionalDependency.isOptional(from, to);
            this.dynamic = false;
        }

        Dependency(Module m, boolean optional, boolean dynamic) {
            this.module = m != null ? m.group() : null;
            this.optional = optional;
            this.dynamic = dynamic;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Dependency)) {
                return false;
            }
            if (this == obj) {
                return true;
            }

            Dependency d = (Dependency) obj;
            if (this.module != d.module) {
                return false;
            } else {
                return this.optional == d.optional && this.dynamic == d.dynamic;
            }
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 19 * hash + (this.module != null ? this.module.hashCode() : 0);
            hash = 19 * hash + (this.optional ? 1 : 0);
            hash = 19 * hash + (this.dynamic ? 1 : 0);
            return hash;
        }

        @Override
        public int compareTo(Dependency d) {
            if (this.equals(d)) {
                return 0;
            }

            // Hard static > hard dynamic > optional static > optional dynamic
            if (this.module == d.module) {
                if (this.optional == d.optional) {
                    return this.dynamic ? -1 : 1;
                } else {
                    return this.optional ? -1 : 1;
                }
            } else if (this.module != null && d.module != null) {
                return (this.module.compareTo(d.module));
            } else {
                return (this.module == null) ? -1 : 1;
            }
        }

        @Override
        public String toString() {
            String s = module.name();
            if (dynamic && optional) {
                s += " (dynamic)";
            } else if (optional) {
                s += " (optional)";
            }
            return s;
        }
    }

    static class Reference implements Comparable<Reference> {

        private final Klass referrer, referree;

        Reference(Klass referrer, Klass referree) {
            this.referrer = referrer;
            this.referree = referree;
        }

        Klass referrer() {
            return referrer;
        }

        Klass referree() {
            return referree;
        }

        @Override
        public int hashCode() {
            return referrer.hashCode() ^ referree.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Reference)) {
                return false;
            }
            if (this == obj) {
                return true;
            }

            Reference r = (Reference) obj;
            return (this.referrer.equals(r.referrer) &&
                    this.referree.equals(r.referree));
        }

        @Override
        public int compareTo(Reference r) {
            int ret = referrer.compareTo(r.referrer);
            if (ret == 0) {
                ret = referree.compareTo(r.referree);
            }
            return ret;
        }
    }

    interface Visitor<P> {

        public void preVisit(Module m, P param);

        public void postVisit(Module m, Module child, P param);
    }
    private static boolean traceOn = System.getProperty("classanalyzer.debug") != null;

    private static void trace(String format, Object... params) {
        System.err.format(format, params);
    }
}
