/*
 * Copyright (c) 2017, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.tools.jar;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Exports;
import java.lang.module.ModuleDescriptor.Opens;
import java.lang.module.ModuleDescriptor.Provides;
import java.lang.module.ModuleDescriptor.Requires;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static java.util.jar.JarFile.MANIFEST_NAME;
import static sun.tools.jar.Main.VERSIONS_DIR;
import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
import static sun.tools.jar.Main.MODULE_INFO;
import static sun.tools.jar.Main.getMsg;
import static sun.tools.jar.Main.formatMsg;
import static sun.tools.jar.Main.formatMsg2;
import static sun.tools.jar.Main.toBinaryName;

final class Validator {

    private final Map<String,FingerPrint> classes = new HashMap<>();
    private final Main main;
    private final ZipFile zf;
    private boolean isValid = true;
    private Set<String> concealedPkgs = Collections.emptySet();
    private ModuleDescriptor md;
    private String mdName;

    private Validator(Main main, ZipFile zf) {
        this.main = main;
        this.zf = zf;
        checkModuleDescriptor(MODULE_INFO);
    }

    static boolean validate(Main main, ZipFile zf) throws IOException {
        return new Validator(main, zf).validate();
    }

    private boolean validate() {
        try {
            zf.stream()
              .filter(e -> e.getName().endsWith(".class"))
              .map(this::getFingerPrint)
              .filter(FingerPrint::isClass)    // skip any non-class entry
              .collect(Collectors.groupingBy(
                      FingerPrint::mrversion,
                      TreeMap::new,
                      Collectors.toMap(FingerPrint::className,
                                       Function.identity(),
                                       this::sameNameFingerPrint)))
              .forEach((version, entries) -> {
                      if (version == 0)
                          validateBase(entries);
                      else
                          validateVersioned(entries);
                  });
        } catch (InvalidJarException e) {
            errorAndInvalid(e.getMessage());
        }
        return isValid;
    }

    static class InvalidJarException extends RuntimeException {
        private static final long serialVersionUID = -3642329147299217726L;
        InvalidJarException(String msg) {
            super(msg);
        }
    }

    private FingerPrint sameNameFingerPrint(FingerPrint fp1, FingerPrint fp2) {
        checkClassName(fp1);
        checkClassName(fp2);
        // entries/classes with same name, return fp2 for now ?
        return fp2;
    }

    private FingerPrint getFingerPrint(ZipEntry ze) {
        // figure out the version and basename from the ZipEntry
        String ename = ze.getName();
        String bname = ename;
        int version = 0;

        if (ename.startsWith(VERSIONS_DIR)) {
            int n = ename.indexOf("/", VERSIONS_DIR_LENGTH);
            if (n == -1) {
                throw new InvalidJarException(
                    formatMsg("error.validator.version.notnumber", ename));
            }
            try {
                version = Integer.parseInt(ename, VERSIONS_DIR_LENGTH, n, 10);
            } catch (NumberFormatException x) {
                throw new InvalidJarException(
                    formatMsg("error.validator.version.notnumber", ename));
            }
            if (n == ename.length()) {
                throw new InvalidJarException(
                    formatMsg("error.validator.entryname.tooshort", ename));
            }
            bname = ename.substring(n + 1);
        }

        // return the cooresponding fingerprint entry
        try (InputStream is = zf.getInputStream(ze)) {
            return new FingerPrint(bname, ename, version, is.readAllBytes());
        } catch (IOException x) {
           throw new InvalidJarException(x.getMessage());
        }
    }

    /*
     *  Validates (a) if there is any isolated nested class, and (b) if the
     *  class name in class file (by asm) matches the entry's basename.
     */
    public void validateBase(Map<String, FingerPrint> fps) {
        fps.values().forEach( fp -> {
            if (!checkClassName(fp)) {
                return;
            }
            if (fp.isNestedClass()) {
                checkNestedClass(fp, fps);
            }
            classes.put(fp.className(), fp);
        });
    }

    public void validateVersioned(Map<String, FingerPrint> fps) {

        fps.values().forEach( fp -> {

            // validate the versioned module-info
            if (MODULE_INFO.equals(fp.basename())) {
                checkModuleDescriptor(fp.entryName());
                return;
            }
            // process a versioned entry, look for previous entry with same name
            FingerPrint matchFp = classes.get(fp.className());
            if (matchFp == null) {
                // no match found
                if (fp.isNestedClass()) {
                    checkNestedClass(fp, fps);
                    return;
                }
                if (fp.isPublicClass()) {
                    if (!isConcealed(fp.className())) {
                        errorAndInvalid(formatMsg("error.validator.new.public.class",
                                                  fp.entryName()));
                        return;
                     }
                     // entry is a public class entry in a concealed package
                     warn(formatMsg("warn.validator.concealed.public.class",
                                   fp.entryName()));
                }
                classes.put(fp.className(), fp);
                return;
            }

            // are the two classes/resources identical?
            if (fp.isIdentical(matchFp)) {
                warn(formatMsg("warn.validator.identical.entry", fp.entryName()));
                return;    // it's okay, just takes up room
            }

            // ok, not identical, check for compatible class version and api
            if (fp.isNestedClass()) {
                checkNestedClass(fp, fps);
                return;    // fall through, need check nested public class??
            }
            if (!fp.isCompatibleVersion(matchFp)) {
                errorAndInvalid(formatMsg("error.validator.incompatible.class.version",
                                          fp.entryName()));
                return;
            }
            if (!fp.isSameAPI(matchFp)) {
                errorAndInvalid(formatMsg("error.validator.different.api",
                                          fp.entryName()));
                return;
            }
            if (!checkClassName(fp)) {
                return;
            }
            classes.put(fp.className(), fp);

            return;
        });
    }

    /*
     * Checks whether or not the given versioned module descriptor's attributes
     * are valid when compared against the root/base module descriptor.
     *
     * A versioned module descriptor must be identical to the root/base module
     * descriptor, with two exceptions:
     *  - A versioned descriptor can have different non-public `requires`
     *    clauses of platform ( `java.*` and `jdk.*` ) modules, and
     *  - A versioned descriptor can have different `uses` clauses, even of
    *    service types defined outside of the platform modules.
     */
    private void checkModuleDescriptor(String miName) {
        ZipEntry ze = zf.getEntry(miName);
        if (ze != null) {
            try (InputStream jis = zf.getInputStream(ze)) {
                ModuleDescriptor md = ModuleDescriptor.read(jis);
                // Initialize the base md if it's not yet. A "base" md can be either the
                // root module-info.class or the first versioned module-info.class
                ModuleDescriptor base = this.md;

                if (base == null) {
                    concealedPkgs = new HashSet<>(md.packages());
                    md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
                    md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
                    // must have the implementation class of the services it 'provides'.
                    if (md.provides().stream().map(Provides::providers)
                          .flatMap(List::stream)
                          .filter(p -> zf.getEntry(toBinaryName(p)) == null)
                          .peek(p -> error(formatMsg("error.missing.provider", p)))
                          .count() != 0) {
                        isValid = false;
                        return;
                    }
                    this.md = md;
                    this.mdName = miName;
                    return;
                }

                if (!base.name().equals(md.name())) {
                    errorAndInvalid(getMsg("error.validator.info.name.notequal"));
                }
                if (!base.requires().equals(md.requires())) {
                    Set<Requires> baseRequires = base.requires();
                    for (Requires r : md.requires()) {
                        if (baseRequires.contains(r))
                            continue;
                        if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) {
                            errorAndInvalid(getMsg("error.validator.info.requires.transitive"));
                        } else if (!isPlatformModule(r.name())) {
                            errorAndInvalid(getMsg("error.validator.info.requires.added"));
                        }
                    }
                    for (Requires r : baseRequires) {
                        Set<Requires> mdRequires = md.requires();
                        if (mdRequires.contains(r))
                            continue;
                        if (!isPlatformModule(r.name())) {
                            errorAndInvalid(getMsg("error.validator.info.requires.dropped"));
                        }
                    }
                }
                if (!base.exports().equals(md.exports())) {
                    errorAndInvalid(getMsg("error.validator.info.exports.notequal"));
                }
                if (!base.opens().equals(md.opens())) {
                    errorAndInvalid(getMsg("error.validator.info.opens.notequal"));
                }
                if (!base.provides().equals(md.provides())) {
                    errorAndInvalid(getMsg("error.validator.info.provides.notequal"));
                }
                if (!base.mainClass().equals(md.mainClass())) {
                    errorAndInvalid(formatMsg("error.validator.info.manclass.notequal",
                                              ze.getName()));
                }
                if (!base.version().equals(md.version())) {
                    errorAndInvalid(formatMsg("error.validator.info.version.notequal",
                                              ze.getName()));
                }
            } catch (Exception x) {
                errorAndInvalid(x.getMessage() + " : " + miName);
            }
        }
    }

    private boolean checkClassName(FingerPrint fp) {
        if (fp.className().equals(className(fp.basename()))) {
            return true;
        }
        error(formatMsg2("error.validator.names.mismatch",
                         fp.entryName(), fp.className().replace("/", ".")));
        return isValid = false;
    }

    private boolean checkNestedClass(FingerPrint fp, Map<String, FingerPrint> outerClasses) {
        if (outerClasses.containsKey(fp.outerClassName())) {
            return true;
        }
        // outer class was not available

        error(formatMsg("error.validator.isolated.nested.class", fp.entryName()));
        return isValid = false;
    }

    private boolean isConcealed(String className) {
        if (concealedPkgs.isEmpty()) {
            return false;
        }
        int idx = className.lastIndexOf('/');
        String pkgName = idx != -1 ? className.substring(0, idx).replace('/', '.') : "";
        return concealedPkgs.contains(pkgName);
    }

    private static boolean isPlatformModule(String name) {
        return name.startsWith("java.") || name.startsWith("jdk.");
    }

    private static String className(String entryName) {
        return entryName.endsWith(".class") ? entryName.substring(0, entryName.length() - 6) : null;
    }

    private void error(String msg) {
        main.error(msg);
    }

    private void errorAndInvalid(String msg) {
        main.error(msg);
        isValid = false;
    }

    private void warn(String msg) {
        main.warn(msg);
    }
}