/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (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.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.modules;

import org.openide.util.Utilities;

// THIS CLASS OUGHT NOT USE NbBundle NOR org.openide CLASSES
// OUTSIDE OF openide-util.jar! UI AND FILESYSTEM/DATASYSTEM
// INTERACTIONS SHOULD GO ELSEWHERE.
import java.util.*;


/** A dependency a module can have.
 * @author Jesse Glick
 * @since 1.24
 */
public final class Dependency {
    /** Dependency on another module. */
    public final static int TYPE_MODULE = 1;

    /** Dependency on a package. */
    public final static int TYPE_PACKAGE = 2;

    /** Dependency on Java. */
    public final static int TYPE_JAVA = 3;

    /** Dependency on the IDE. */
    public final static int TYPE_IDE = 4;

    /** Dependency on a token.
     * @see ModuleInfo#getProvides
     * @since 2.3
     */
    public final static int TYPE_REQUIRES = 5;

    /** Comparison by specification version. */
    public final static int COMPARE_SPEC = 1;

    /** Comparison by implementation version. */
    public final static int COMPARE_IMPL = 2;

    /** No comparison, just require the dependency to be present. */
    public final static int COMPARE_ANY = 3;

    /** @deprecated request dependencies on direct modules */
    public static final String IDE_NAME = System.getProperty("org.openide.major.version", "IDE"); // NOI18N

    /** @deprecated request dependencies on direct modules */
    public static final SpecificationVersion IDE_SPEC = makeSpec(
            System.getProperty("org.openide.specification.version")
        ); // NOI18N

    /** @deprecated request dependencies on direct modules */
    public static final String IDE_IMPL = System.getProperty("org.openide.version"); // NOI18N

    /** Name, for purposes of dependencies, of the Java platform. */
    public static final String JAVA_NAME = "Java"; // NOI18N

    /** Specification version of the Java platform. */
    public static final SpecificationVersion JAVA_SPEC = makeSpec(System.getProperty("java.specification.version")); // NOI18N

    /** Implementation version of the Java platform. */
    public static final String JAVA_IMPL = System.getProperty("java.version"); // NOI18N

    /** Name, for purposes of dependencies, of the Java VM. */
    public static final String VM_NAME = "VM"; // NOI18N

    /** Specification version of the Java VM. */
    public static final SpecificationVersion VM_SPEC = makeSpec(System.getProperty("java.vm.specification.version")); // NOI18N

    /** Implementation version of the Java VM. */
    public static final String VM_IMPL = System.getProperty("java.vm.version"); // NOI18N
    private final int type;
    private final int comparison;
    private final String name;
    private final String version;

    private Dependency(int type, String name, int comparison, String version) {
        this.type = type;
        this.name = name.intern();
        this.comparison = comparison;
        this.version = (version != null) ? version.intern() : null;
    }

    /** Verify the format of a code name.
     * Caller specifies whether a slash plus release version is permitted in this context.
     */
    private static void checkCodeName(String codeName, boolean slashOK)
    throws IllegalArgumentException {
        String base;
        int slash = codeName.indexOf('/'); // NOI18N

        if (slash == -1) {
            base = codeName;
        } else {
            if (!slashOK) {
                throw new IllegalArgumentException("No slash permitted in: " + codeName); // NOI18N
            }

            base = codeName.substring(0, slash);

            String rest = codeName.substring(slash + 1);
            int dash = rest.indexOf('-'); // NOI18N

            try {
                if (dash == -1) {
                    int release = Integer.parseInt(rest);

                    if (release < 0) {
                        throw new IllegalArgumentException("Negative release number: " + codeName); // NOI18N
                    }
                } else {
                    int release = Integer.parseInt(rest.substring(0, dash));
                    int releaseMax = Integer.parseInt(rest.substring(dash + 1));

                    if (release < 0) {
                        throw new IllegalArgumentException("Negative release number: " + codeName); // NOI18N
                    }

                    if (releaseMax <= release) {
                        throw new IllegalArgumentException("Release number range must be increasing: " + codeName); // NOI18N
                    }
                }
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException(e.toString());
            }
        }

        // Now check that the rest is a valid package.
        StringTokenizer tok = new StringTokenizer(base, ".", true); // NOI18N

        if ((tok.countTokens() % 2) == 0) {
            throw new NumberFormatException("Even number of pieces: " + base); // NOI18N
        }

        boolean expectingPath = true;

        while (tok.hasMoreTokens()) {
            if (expectingPath) {
                expectingPath = false;

                String nt = tok.nextToken();
                if (!Utilities.isJavaIdentifier(nt) &&  !"enum".equals (nt)) { // NOI18N
                    throw new IllegalArgumentException("Bad package component in " + base); // NOI18N
                }
            } else {
                if (!".".equals(tok.nextToken())) { // NOI18N
                    throw new NumberFormatException("Expected dot in code name: " + base); // NOI18N
                }

                expectingPath = true;
            }
        }
    }

    /** Parse dependencies from tags.
    * @param type like Dependency.type
    * @param body actual text of tag body; if <code>null</code>, returns nothing
    * @return a set of dependencies
    * @throws IllegalArgumentException if they are malformed or inconsistent
    */
    public static Set create(int type, String body) throws IllegalArgumentException {
        if (body == null) {
            return Collections.EMPTY_SET;
        }

        Set deps = new HashSet(5); // Set<Dependency>

        // First split on commas.
        StringTokenizer tok = new StringTokenizer(body, ","); // NOI18N

        if (!tok.hasMoreTokens()) {
            throw new IllegalArgumentException("No deps given: \"" + body + "\""); // NOI18N
        }

        Map depsByKey = new HashMap(10); // Map<DependencyKey,Dependency>

        while (tok.hasMoreTokens()) {
            String onedep = tok.nextToken();
            StringTokenizer tok2 = new StringTokenizer(onedep, " \t\n\r"); // NOI18N

            if (!tok2.hasMoreTokens()) {
                throw new IllegalArgumentException("No name in dependency: " + onedep); // NOI18N
            }

            String name = tok2.nextToken();
            int comparison;
            String version;

            if (tok2.hasMoreTokens()) {
                String compthing = tok2.nextToken();

                if (compthing.equals(">")) { // NOI18N
                    comparison = Dependency.COMPARE_SPEC;
                } else if (compthing.equals("=")) { // NOI18N
                    comparison = Dependency.COMPARE_IMPL;
                } else {
                    throw new IllegalArgumentException("Strange comparison string: " + compthing); // NOI18N
                }

                if (!tok2.hasMoreTokens()) {
                    throw new IllegalArgumentException("Comparison string without version: " + onedep); // NOI18N
                }

                version = tok2.nextToken();

                if (tok2.hasMoreTokens()) {
                    throw new IllegalArgumentException("Trailing garbage in dependency: " + onedep); // NOI18N
                }

                if (comparison == Dependency.COMPARE_SPEC) {
                    try {
                        new SpecificationVersion(version);
                    } catch (NumberFormatException nfe) {
                        throw new IllegalArgumentException(nfe.toString());
                    }
                }
            } else {
                comparison = Dependency.COMPARE_ANY;
                version = null;
            }

            if (type == Dependency.TYPE_MODULE) {
                checkCodeName(name, true);

                if ((name.indexOf('-') != -1) && (comparison == Dependency.COMPARE_IMPL)) {
                    throw new IllegalArgumentException(
                        "Cannot have an implementation dependency on a ranged release version: " + onedep
                    ); // NOI18N
                }
            } else if (type == Dependency.TYPE_PACKAGE) {
                int idx = name.indexOf('[');

                if (idx != -1) {
                    if (idx > 0) {
                        checkCodeName(name.substring(0, idx), false);
                    }

                    if (name.charAt(name.length() - 1) != ']') {
                        throw new IllegalArgumentException("No close bracket on package dep: " + name); // NOI18N
                    }

                    checkCodeName(name.substring(idx + 1, name.length() - 1), false);
                } else {
                    checkCodeName(name, false);
                }

                if ((idx == 0) && (comparison != Dependency.COMPARE_ANY)) {
                    throw new IllegalArgumentException(
                        "Cannot use a version comparison on a package dependency when only a sample class is given"
                    ); // NOI18N
                }

                if ((idx > 0) && (name.substring(idx + 1, name.length() - 1).indexOf('.') != -1)) {
                    throw new IllegalArgumentException(
                        "Cannot have a sample class with dots when package is specified"
                    ); // NOI18N
                }
            } else if (type == Dependency.TYPE_JAVA) {
                if (!(name.equals(JAVA_NAME) || name.equals(VM_NAME))) { // NOI18N
                    throw new IllegalArgumentException("Java dependency must be on \"Java\" or \"VM\": " + name); // NOI18N
                }

                if (comparison == Dependency.COMPARE_ANY) {
                    throw new IllegalArgumentException("Must give a comparison for a Java dep: " + body); // NOI18N
                }
            } else if (type == Dependency.TYPE_IDE) {
                if (!(name.equals("IDE"))) { // NOI18N

                    int slash = name.indexOf("/"); // NOI18N
                    boolean ok;

                    if (slash == -1) {
                        ok = false;
                    } else {
                        if (!name.substring(0, slash).equals("IDE")) { // NOI18N
                            ok = false;
                        }

                        try {
                            int v = Integer.parseInt(name.substring(slash + 1));
                            ok = (v >= 0);
                        } catch (NumberFormatException e) {
                            ok = false;
                        }
                    }

                    if (!ok) {
                        throw new IllegalArgumentException("Invalid IDE dependency: " + name); // NOI18N
                    }
                }

                if (comparison == Dependency.COMPARE_ANY) {
                    throw new IllegalArgumentException("Must give a comparison for an IDE dep: " + body); // NOI18N
                }
            } else if (type == Dependency.TYPE_REQUIRES) {
                if (comparison != Dependency.COMPARE_ANY) {
                    throw new IllegalArgumentException("Cannot give a comparison for a token requires dep: " + body); // NOI18N
                }

                checkCodeName(name, false);
            } else {
                throw new IllegalArgumentException("unknown type"); // NOI18N
            }

            Dependency nue = new Dependency(type, name, comparison, version);
            DependencyKey key = new DependencyKey(nue);

            if (depsByKey.containsKey(key)) {
                throw new IllegalArgumentException(
                    "Dependency " + nue + " duplicates the similar dependency " + depsByKey.get(key)
                ); // NOI18N
            } else {
                deps.add(nue);
                depsByKey.put(key, nue);
            }
        }

        return deps;
    }

    /** Get the type. */
    public final int getType() {
        return type;
    }

    /** Get the name of the depended-on object. */
    public final String getName() {
        return name;
    }

    /** Get the comparison type. */
    public final int getComparison() {
        return comparison;
    }

    /** Get the version to compare against (or null). */
    public final String getVersion() {
        return version;
    }

    /** Overridden to compare contents. */
    public boolean equals(Object o) {
        if (o.getClass() != Dependency.class) {
            return false;
        }

        Dependency d = (Dependency) o;

        return (type == d.type) && (comparison == d.comparison) && name.equals(d.name) &&
        Utilities.compareObjects(version, d.version);
    }

    /** Overridden to hash by contents. */
    public int hashCode() {
        return 772067 ^ type ^ name.hashCode();
    }

    /** Unspecified string representation for debugging. */
    public String toString() {
        StringBuffer buf = new StringBuffer(100);

        if (type == TYPE_MODULE) {
            buf.append("module "); // NOI18N
        } else if (type == TYPE_PACKAGE) {
            buf.append("package "); // NOI18N
        } else if (type == TYPE_REQUIRES) {
            buf.append("token "); // NOI18N
        }

        buf.append(name);

        if (comparison == COMPARE_IMPL) {
            buf.append(" = "); // NOI18N
            buf.append(version);
        } else if (comparison == COMPARE_SPEC) {
            buf.append(" > "); // NOI18N
            buf.append(version);
        }

        return buf.toString();
    }

    /** Try to make a specification version from a string.
     * Deal with errors gracefully and try to recover something from it.
     * E.g. "1.4.0beta" is technically erroneous; correct to "1.4.0".
     */
    private static SpecificationVersion makeSpec(String vers) {
        if (vers != null) {
            try {
                return new SpecificationVersion(vers);
            } catch (NumberFormatException nfe) {
                System.err.println("WARNING: invalid specification version: " + vers); // NOI18N
            }

            do {
                vers = vers.substring(0, vers.length() - 1);

                try {
                    return new SpecificationVersion(vers);
                } catch (NumberFormatException nfe) {
                    // ignore
                }
            } while (vers.length() > 0);
        }

        // Nothing decent in it at all; use zero.
        return new SpecificationVersion("0"); // NOI18N
    }

    /** Key for checking for duplicates among dependencies.
     * The unique characteristics of a dependency are:
     * 1. The basic name. No release versions, no sample classes for packages
     * (though if you specify only the class and not the package, this is different).
     * 2. The type of dependency (module, package, etc.).
     * Sample things which ought not be duplicated:
     * 1. Sample classes within a package.
     * 2. The same module with different release versions (use ranged releases as needed).
     * 3. Impl & spec comparisons (the impl comparison is stricter anyway).
     * 4. Different versions of the same thing (makes no sense).
     */
    private static final class DependencyKey {
        private final int type;
        private final String name;

        public DependencyKey(Dependency d) {
            type = d.getType();

            switch (type) {
            case TYPE_MODULE:
            case TYPE_IDE:

                String codeName = d.getName();
                int idx = codeName.lastIndexOf('/');

                if (idx == -1) {
                    name = codeName;
                } else {
                    name = codeName.substring(0, idx);
                }

                break;

            case TYPE_PACKAGE:

                String pkgName = d.getName();
                idx = pkgName.indexOf('[');

                if (idx != -1) {
                    if (idx == 0) {
                        // [org.apache.jasper.Constants]
                        // Keep the [] only to differentiate it from a package name:
                        name = pkgName;
                    } else {
                        // org.apache.jasper[Constants]
                        name = pkgName.substring(0, idx);
                    }
                } else {
                    // org.apache.jasper
                    name = pkgName;
                }

                break;

            default:

                // TYPE_REQUIRES, TYPE_JAVA
                name = d.getName();

                break;
            }

            //System.err.println("Key for " + d + " is " + this);
        }

        public int hashCode() {
            return name.hashCode();
        }

        public boolean equals(Object o) {
            return (o instanceof DependencyKey) && ((DependencyKey) o).name.equals(name) &&
            (((DependencyKey) o).type == type);
        }

        public String toString() {
            return "DependencyKey[" + name + "," + type + "]"; // NOI18N
        }
    }
}
