/*
 * 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;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.Module.PackageExport;
import org.openide.modules.Dependency;
import org.openide.modules.PatchFor;
import org.openide.modules.SpecificationVersion;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;

/** Information about essential properties of a module.
 *
 * @author Jaroslav Tulach <jtulach@netbeans.org>
 */
class ModuleData {
    private final static PackageExport[] ZERO_PACKAGE_ARRAY = new PackageExport[0];
    private final static String[] ZERO_STRING_ARRAY = new String[0];

    private final String codeName;
    private final String codeNameBase;
    private final int codeNameRelease;
    private final String implVersion;
    private final String buildVersion;
    private final Set<String> friendNames;
    private final SpecificationVersion specVers;
    private final PackageExport[] publicPackages;
    private final String[] provides;
    private final Dependency[] dependencies;
    private final Set<String> coveredPackages;
    private final String agentClass;
    private final String fragmentHostCodeName;
    
    ModuleData(Manifest mf, Module forModule) throws InvalidException {
        Attributes attr = mf.getMainAttributes();
        // Code name
        codeName = attr.getValue("OpenIDE-Module"); // NOI18N
        if (codeName == null) {
            InvalidException e = new InvalidException("Not a module: no OpenIDE-Module tag in manifest of " + /* #17629: important! */ this, mf); // NOI18N
            // #29393: plausible user mistake, deal with it politely.
            Exceptions.attachLocalizedMessage(e,
                NbBundle.getMessage(Module.class,
                "EXC_not_a_module",
                this.toString()));
            throw e;
        }
        forModule.assignData(this);
        try {
            // This has the side effect of checking syntax:
            if (codeName.indexOf(',') != -1) {
                throw new InvalidException("Illegal code name syntax parsing OpenIDE-Module: " + codeName); // NOI18N
            }
            Object[] cnParse = Util.parseCodeName(codeName);
            codeNameBase = (String) cnParse[0];
            Set<?> deps = forModule.getManager().loadDependencies(codeNameBase);
            boolean verifyCNBs = deps == null;
            if (verifyCNBs) {
                Dependency.create(Dependency.TYPE_MODULE, codeName);
            }
            codeNameRelease = (cnParse[1] != null) ? ((Integer) cnParse[1]).intValue() : -1;
            if (cnParse[2] != null) {
                throw new NumberFormatException(codeName);
            }
            // Spec vers
            String specVersS = attr.getValue("OpenIDE-Module-Specification-Version"); // NOI18N
            if (specVersS != null) {
                try {
                    specVers = new SpecificationVersion(specVersS);
                } catch (NumberFormatException nfe) {
                    throw (InvalidException) new InvalidException("While parsing OpenIDE-Module-Specification-Version: " + nfe.toString()).initCause(nfe); // NOI18N
                }
            } else {
                specVers = null;
            }
            String iv = attr.getValue("OpenIDE-Module-Implementation-Version"); // NOI18N
            implVersion = iv == null ? "" : iv;
            String bld = attr.getValue("OpenIDE-Module-Build-Version"); // NOI18N
            buildVersion = bld == null ? implVersion : bld;
            
            this.provides = computeProvides(forModule, attr, verifyCNBs, false);

            // Exports
            String exportsS = attr.getValue("OpenIDE-Module-Public-Packages"); // NOI18N
            if (exportsS != null) {
                if (exportsS.trim().equals("-")) { // NOI18N
                    publicPackages = ZERO_PACKAGE_ARRAY;
                } else {
                    StringTokenizer tok = new StringTokenizer(exportsS, ", "); // NOI18N
                    List<Module.PackageExport> exports = new ArrayList<Module.PackageExport>(Math.max(tok.countTokens(), 1));
                    while (tok.hasMoreTokens()) {
                        String piece = tok.nextToken();
                        if (piece.endsWith(".*")) { // NOI18N
                            String pkg = piece.substring(0, piece.length() - 2);
                            if (verifyCNBs) {
                                Dependency.create(Dependency.TYPE_MODULE, pkg);
                            }
                            if (pkg.lastIndexOf('/') != -1) {
                                throw new IllegalArgumentException("Illegal OpenIDE-Module-Public-Packages: " + exportsS); // NOI18N
                            }
                            exports.add(new Module.PackageExport(pkg.replace('.', '/') + '/', false));
                        } else if (piece.endsWith(".**")) { // NOI18N
                            String pkg = piece.substring(0, piece.length() - 3);
                            if (verifyCNBs) {
                                Dependency.create(Dependency.TYPE_MODULE, pkg);
                            }
                            if (pkg.lastIndexOf('/') != -1) {
                                throw new IllegalArgumentException("Illegal OpenIDE-Module-Public-Packages: " + exportsS); // NOI18N
                            }
                            exports.add(new Module.PackageExport(pkg.replace('.', '/') + '/', true));
                        } else {
                            throw new IllegalArgumentException("Illegal OpenIDE-Module-Public-Packages: " + exportsS); // NOI18N
                        }
                    }
                    if (exports.isEmpty()) {
                        throw new IllegalArgumentException("Illegal OpenIDE-Module-Public-Packages: " + exportsS); // NOI18N
                    }
                    publicPackages = exports.toArray(new Module.PackageExport[exports.size()]);
                }
            } else {
                // XXX new link?
                Util.err.log(Level.WARNING, "module {0} does not declare OpenIDE-Module-Public-Packages "
                    + "in its manifest, so all packages are considered public by default: "
                    + "http://bits.netbeans.org/dev/javadoc/org-openide-modules/org/openide/modules/doc-files/api.html#how-vers", 
                    codeNameBase
                );
                publicPackages = null;
            }

            {
                HashSet<String> set = null;
                // friends 
                String friends = attr.getValue("OpenIDE-Module-Friends"); // NOI18N
                if (friends != null) {
                    StringTokenizer tok = new StringTokenizer(friends, ", "); // NOI18N
                    set = new HashSet<String>();
                    while (tok.hasMoreTokens()) {
                        String piece = tok.nextToken();
                        if (piece.indexOf('/') != -1) {
                            throw new IllegalArgumentException("May specify only module code name bases in OpenIDE-Module-Friends, not major release versions: " + piece); // NOI18N
                        }
                        if (verifyCNBs) {
                            // Indirect way of checking syntax:
                            Dependency.create(Dependency.TYPE_MODULE, piece);
                        }
                        // OK, add it.
                        set.add(piece);
                    }
                    if (set.isEmpty()) {
                        throw new IllegalArgumentException("Empty OpenIDE-Module-Friends: " + friends); // NOI18N
                    }
                    if (publicPackages == null || publicPackages.length == 0) {
                        throw new IllegalArgumentException("No use specifying OpenIDE-Module-Friends without any public packages: " + friends); // NOI18N
                    }
                }
                this.friendNames = set;
            }
            this.dependencies = initDeps(forModule, deps, attr);
            String classLoader = attr.getValue(PatchFor.MANIFEST_FRAGMENT_HOST); // NOI18N
            if (classLoader != null) {
                Object[] clParse = Util.parseCodeName(classLoader);
                String frag = (String)clParse[0];
                if (frag != null) {
                    if ((frag = frag.trim()).isEmpty()) {
                        frag = null;
                    }
                }
                this.fragmentHostCodeName = frag;
                if (verifyCNBs && frag != null) {
                    // Indirect way of checking syntax:
                    Dependency.create(Dependency.TYPE_MODULE, fragmentHostCodeName);
                }
            } else {
                fragmentHostCodeName = null;
            }
        } catch (IllegalArgumentException iae) {
            throw (InvalidException) new InvalidException("While parsing " + codeName + " a dependency attribute: " + iae.toString()).initCause(iae); // NOI18N
        }
        this.coveredPackages = new HashSet<String>();
        this.agentClass = attr.getValue("Agent-Class");
    }
    
    ModuleData(Manifest mf, NetigsoModule m) throws InvalidException {
        final String symbName = getMainAttribute(mf, "Bundle-SymbolicName"); // NOI18N
        if (symbName == null) {
            throw new InvalidException("Not an OSGi bundle: " + m);
        }
        m.assignData(this);
        this.codeName = symbName.replace('-', '_');
        int slash = codeName.lastIndexOf('/');
        if (slash != -1) {
            this.codeNameRelease = Integer.parseInt(symbName.substring(slash + 1));
        } else {
            this.codeNameRelease = -1;
        }
        String v = getMainAttribute(mf, "Bundle-Version"); // NOI18N
        if (v == null) {
            Logger.getLogger(ModuleData.class.getName()).log(Level.WARNING, "No Bundle-Version for {0}", m);
            this.specVers = new SpecificationVersion(v = "0.0");
        } else {
            this.specVers = computeVersion(v);
        }
        this.codeNameBase = codeName;
        String iv = getMainAttribute(mf, "OpenIDE-Module-Implementation-Version"); // NOI18N
        this.implVersion = iv == null ? v : iv;
        String bld = getMainAttribute(mf, "OpenIDE-Module-Build-Version"); // NOI18N
        this.buildVersion = bld == null ? implVersion : bld;
        this.friendNames = Collections.emptySet();
        this.publicPackages = null;
        this.provides = computeProvides(m, mf.getMainAttributes(), false, true);
        this.dependencies = computeImported(mf.getMainAttributes());
        this.coveredPackages = new HashSet<String>();
        this.agentClass = getMainAttribute(mf, "Agent-Class"); // NOI18N
        this.fragmentHostCodeName = null;
    }
    
    ModuleData(ObjectInput dis) throws IOException {
        try {
            this.codeName = dis.readUTF();
            this.codeNameBase = dis.readUTF();
            this.codeNameRelease = dis.readInt();
            this.coveredPackages = readStrings(dis, new HashSet<String>(), true);
            this.dependencies = (Dependency[]) dis.readObject();
            this.implVersion = dis.readUTF();
            this.buildVersion = dis.readUTF();
            this.provides = readStrings(dis);
            this.friendNames = readStrings(dis, new HashSet<String>(), false);
            this.specVers = new SpecificationVersion(dis.readUTF());
            this.publicPackages = Module.PackageExport.read(dis);
            this.agentClass = dis.readUTF();
            String s = dis.readUTF();
            if (s != null) {
                s = s.trim();
            }
            this.fragmentHostCodeName = s == null || s.isEmpty() ? null : s;
        } catch (ClassNotFoundException cnfe) {
            throw new IOException(cnfe);
        }
    }
    
    void write(ObjectOutput dos) throws IOException {
        dos.writeUTF(codeName);
        dos.writeUTF(codeNameBase);
        dos.writeInt(codeNameRelease);
        writeStrings(dos, coveredPackages);
        dos.writeObject(dependencies);
        dos.writeUTF(implVersion);
        dos.writeUTF(buildVersion);
        writeStrings(dos, provides);
        writeStrings(dos, friendNames);
        dos.writeUTF(specVers != null ? specVers.toString() : "0");
        Module.PackageExport.write(dos, publicPackages);
        dos.writeUTF(agentClass == null ? "" : agentClass);
        dos.writeUTF(fragmentHostCodeName == null ? "" : fragmentHostCodeName);
    }

    private Dependency[] computeImported(Attributes attr) {
        String pkgs = attr.getValue("Import-Package"); // NOI18N
        List<Dependency> arr = null;
        if (pkgs != null) {
            arr = new ArrayList<Dependency>();
            StringTokenizer tok = createTokenizer(pkgs); // NOI18N
            while (tok.hasMoreElements()) {
                String dep = beforeSemicolon(tok);
                arr.addAll(Dependency.create(Dependency.TYPE_RECOMMENDS, dep));
            }
        }
        String recomm = attr.getValue("Require-Bundle"); // NOI18N
        if (recomm != null) {
            if (arr == null) {
                arr = new ArrayList<Dependency>();
            }
            StringTokenizer tok = createTokenizer(recomm); // NOI18N
            while (tok.hasMoreElements()) {
                String dep = beforeSemicolon(tok);
                arr.addAll(Dependency.create(Dependency.TYPE_RECOMMENDS, "cnb." + dep)); // NOI18N
            }
        }
        return arr == null ? null : arr.toArray(new Dependency[0]);
    }

    private static StringTokenizer createTokenizer(String osgiDep) {
        for (;;) {
            int first = osgiDep.indexOf('"');
            if (first == -1) {
                break;
            }
            int second = osgiDep.indexOf('"', first + 1);
            if (second == -1) {
                break;
            }
            osgiDep = osgiDep.substring(0, first - 1) + osgiDep.substring(second + 1);
        }
        
        return new StringTokenizer(osgiDep, ",");
    }

    private static String beforeSemicolon(StringTokenizer tok) {
        String dep = tok.nextToken().trim();
        int semicolon = dep.indexOf(';');
        if (semicolon >= 0) {
            dep = dep.substring(0, semicolon);
        }
        return dep.replace('-', '_');
    }
    
    private String[] computeExported(boolean useOSGi, Collection<String> arr, Attributes attr) {
        if (!useOSGi) {
            return arr.toArray(ZERO_STRING_ARRAY);
        }
        String pkgs = attr.getValue("Export-Package"); // NOI18N
        if (pkgs == null) {
            return arr.toArray(ZERO_STRING_ARRAY);
        }
        StringTokenizer tok = createTokenizer(pkgs); // NOI18N
        while (tok.hasMoreElements()) {
            arr.add(beforeSemicolon(tok));
        }
        return arr.toArray(ZERO_STRING_ARRAY);
    }
    
    private String[] computeProvides(
        Module forModule, Attributes attr, boolean verifyCNBs, boolean useOSGi
    ) throws InvalidException, IllegalArgumentException {
        Set<String> arr = new LinkedHashSet<String>();
        // Token provides
        String providesS = attr.getValue("OpenIDE-Module-Provides"); // NOI18N
        if (providesS != null) {
            StringTokenizer tok = new StringTokenizer(providesS, ", "); // NOI18N
            int expCount = tok.countTokens();
            while (tok.hasMoreTokens()) {
                String provide = tok.nextToken();
                if (provide.indexOf(',') != -1) {
                    throw new InvalidException("Illegal code name syntax parsing OpenIDE-Module-Provides: " + provide); // NOI18N
                }
                if (verifyCNBs) {
                    Dependency.create(Dependency.TYPE_MODULE, provide);
                }
                if (provide.lastIndexOf('/') != -1) throw new IllegalArgumentException("Illegal OpenIDE-Module-Provides: " + provide); // NOI18N
                arr.add(provide);
            }
            if (arr.size() != expCount) {
                throw new IllegalArgumentException("Duplicate entries in OpenIDE-Module-Provides: " + providesS); // NOI18N
            }
        }
        String[] additionalProvides = forModule.getManager().refineProvides (forModule);
        if (additionalProvides != null) {
            arr.addAll (Arrays.asList (additionalProvides));
        }
        arr.add("cnb." + getCodeNameBase()); // NOI18N
        return computeExported(useOSGi, arr, attr);
    }
    
    /**
     * Initializes dependencies of this module
     *
     * @param knownDeps Set<Dependency> of this module known from different
     * source, can be null
     * @param attr attributes in manifest to parse if knownDeps is null
     */
    private Dependency[] initDeps(Module forModule, Set<?> knownDeps, Attributes attr)
        throws IllegalStateException, IllegalArgumentException {
        if (knownDeps != null) {
            return knownDeps.toArray(new Dependency[knownDeps.size()]);
        }

        // deps
        Set<Dependency> deps = new HashSet<Dependency>(20);
        // First convert IDE/1 -> org.openide/1, so we never have to deal with
        // "IDE deps" internally:
        @SuppressWarnings(value = "deprecation")
        Set<Dependency> openideDeps = Dependency.create(Dependency.TYPE_IDE, attr.getValue("OpenIDE-Module-IDE-Dependencies")); // NOI18N
        if (!openideDeps.isEmpty()) {
            // If empty, leave it that way; NbInstaller will add it anyway.
            Dependency d = openideDeps.iterator().next();
            String name = d.getName();
            if (!name.startsWith("IDE/")) {
                throw new IllegalStateException("Weird IDE dep: " + name); // NOI18N
            }
            deps.addAll(Dependency.create(Dependency.TYPE_MODULE, "org.openide/" + name.substring(4) + " > " + d.getVersion())); // NOI18N
            if (deps.size() != 1) {
                throw new IllegalStateException("Should be singleton: " + deps); // NOI18N
            }
            Util.err.log(Level.WARNING, "the module {0} uses OpenIDE-Module-IDE-Dependencies which is deprecated. See http://openide.netbeans.org/proposals/arch/modularize.html", codeNameBase); // NOI18N
        }
        deps.addAll(Dependency.create(Dependency.TYPE_JAVA, attr.getValue("OpenIDE-Module-Java-Dependencies"))); // NOI18N
        deps.addAll(Dependency.create(Dependency.TYPE_MODULE, attr.getValue("OpenIDE-Module-Module-Dependencies"))); // NOI18N
        String pkgdeps = attr.getValue("OpenIDE-Module-Package-Dependencies"); // NOI18N
        if (pkgdeps != null) {
            // XXX: Util.err.log(ErrorManager.WARNING, "Warning: module " + codeNameBase + " uses the OpenIDE-Module-Package-Dependencies 
            // manifest attribute, which is now deprecated: XXX URL TBD");
            deps.addAll(Dependency.create(Dependency.TYPE_PACKAGE, pkgdeps)); // NOI18N
        }
        deps.addAll(Dependency.create(Dependency.TYPE_REQUIRES, attr.getValue("OpenIDE-Module-Requires"))); // NOI18N
        deps.addAll(Dependency.create(Dependency.TYPE_NEEDS, attr.getValue("OpenIDE-Module-Needs"))); // NOI18N
        deps.addAll(Dependency.create(Dependency.TYPE_RECOMMENDS, attr.getValue("OpenIDE-Module-Recommends"))); // NOI18N
        forModule.refineDependencies(deps);
        return deps.toArray(new Dependency[deps.size()]);
    }
    
    final String getFragmentHostCodeName() {
        return fragmentHostCodeName;
    }

    final String getCodeName() {
        return codeName;
    }
    
    final String getCodeNameBase() {
        return codeNameBase;
    }

    final int getCodeNameRelease() {
        return codeNameRelease;
    }

    final String[] getProvides() {
        return provides;
    }

    final SpecificationVersion getSpecificationVersion() {
        return specVers;
    }

    final PackageExport[] getPublicPackages() {
        return publicPackages;
    }

    final Set<String> getFriendNames() {
        return friendNames;
    }

    final Dependency[] getDependencies() {
        return dependencies;
    }

    final String getBuildVersion() {
        return buildVersion.isEmpty() ? null : buildVersion;
    }

    final String getImplementationVersion() {
        return implVersion.isEmpty() ? null : implVersion;
    }
    
    void registerCoveredPackages(Set<String> known) {
        assert coveredPackages.isEmpty();
        coveredPackages.addAll(known);
    }

    Set<String> getCoveredPackages() {
        return coveredPackages.isEmpty() ? null : coveredPackages;
    }

    private <T extends Collection<String>> T readStrings(
        DataInput dis, T set, boolean returnEmpty
    ) throws IOException {
        int cnt = dis.readInt();
        if (!returnEmpty && cnt == 0) {
            return null;
        }
        while (cnt-- > 0) {
            set.add(dis.readUTF());
        }
        return set;
    }
    private String[] readStrings(ObjectInput dis) throws IOException {
        List<String> arr = new ArrayList<String>();
        readStrings(dis, arr, false);
        return arr.toArray(new String[arr.size()]);
    }
    private void writeStrings(DataOutput dos, Collection<String> set) 
    throws IOException {
        if (set == null) {
            dos.writeInt(0);
            return;
        }
        dos.writeInt(set.size());
        for (String s : set) {
            dos.writeUTF(s);
        }
    }
    private void writeStrings(ObjectOutput dos, String[] provides) throws IOException {
        writeStrings(dos, Arrays.asList(provides));
    }
    
    private static String getMainAttribute(Manifest manifest, String attr) {
        String s = manifest.getMainAttributes().getValue(attr);
        if (s == null) {
            return null;
        }
        int semicolon = s.indexOf(';');
        if (semicolon == -1) {
            return s;
        } else {
            return s.substring(0, semicolon);
        }
    }
    private static SpecificationVersion computeVersion(String v) {
        int pos = -1;
        for (int i = 0; i < 3; i++) {
            pos = v.indexOf('.', pos + 1);
            if (pos == -1) {
                return new SpecificationVersion(v);
            }
        }
        return new SpecificationVersion(v.substring(0, pos));
    }

    final String getAgentClass() {
        return agentClass == null || agentClass.isEmpty() ? null : agentClass;
    }
}
