/*
 * Copyright (c) 2015, 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.
 */

/*
 * Asm plugin testing.
 * @test
 * @summary Test resource transformation.
 * @author Andrei Eremeev
 * @modules java.base/jdk.internal.org.objectweb.asm
 *          jdk.jlink/jdk.tools.jlink.internal
 *          jdk.jlink/jdk.tools.jlink.internal.plugins.asm
 *          jdk.jdeps/com.sun.tools.classfile
 * @build AsmPluginTestBase
 * @run main AddForgetResourcesTest
*/

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import com.sun.tools.classfile.ClassFile;
import com.sun.tools.classfile.Method;
import java.io.UncheckedIOException;
import java.util.Set;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.tools.jlink.internal.plugins.asm.AsmGlobalPool;
import jdk.tools.jlink.internal.plugins.asm.AsmModulePool;
import jdk.tools.jlink.internal.plugins.asm.AsmPool.ResourceFile;
import jdk.tools.jlink.internal.plugins.asm.AsmPool.WritableClassPool;
import jdk.tools.jlink.internal.plugins.asm.AsmPool.WritableResourcePool;
import jdk.tools.jlink.internal.plugins.asm.AsmPools;
import jdk.tools.jlink.plugin.ModuleEntry;
import jdk.tools.jlink.plugin.ModulePool;

public class AddForgetResourcesTest extends AsmPluginTestBase {

    public static void main(String[] args) throws Exception {
        if (!isImageBuild()) {
            System.err.println("Test not run. Not image build.");
            return;
        }
        new AddForgetResourcesTest().test();
    }

    @Override
    public void test() throws Exception {
        TestPlugin[] plugins = new TestPlugin[] {
                new AddClassesPlugin(),
                new AddResourcesPlugin(),
                new ReplaceClassesPlugin(),
                new ReplaceResourcesPlugin(),
                new ForgetClassesPlugin(),
                new ForgetResourcesPlugin(),
                new AddForgetClassesPlugin(),
                new AddForgetResourcesPlugin(),
                new ComboPlugin()
        };
        for (TestPlugin p : plugins) {
            ModulePool out = p.visit(getPool());
            p.test(getPool(), out);
        }
    }

    private static final String SUFFIX = "HELLOWORLD";

    private static class RenameClassVisitor extends ClassVisitor {

        public RenameClassVisitor(ClassWriter cv) {
            super(Opcodes.ASM5, cv);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name + SUFFIX, signature, superName, interfaces);
        }
    }

    private static class AddMethodClassVisitor extends ClassVisitor {

        public AddMethodClassVisitor(ClassWriter cv) {
            super(Opcodes.ASM5, cv);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            this.visitMethod(0, SUFFIX, "()V", null, null);
            super.visit(version, access, name, signature, superName, interfaces);
        }
    }

    private class AddClassesPlugin extends TestPlugin {

        private int expected = 0;

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            WritableClassPool transformedClasses = globalPool.getTransformedClasses();
            expected = globalPool.getClasses().size();
            for (ModuleEntry res : globalPool.getClasses()) {
                ClassReader reader = globalPool.getClassReader(res);
                String className = reader.getClassName();
                if (!className.endsWith("module-info")) {
                    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
                    reader.accept(new RenameClassVisitor(writer), ClassReader.EXPAND_FRAMES);
                    transformedClasses.addClass(writer);
                    ++expected;
                }
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool outResources) {
            Collection<ModuleEntry> inClasses = extractClasses(inResources);
            Collection<ModuleEntry> outClasses = extractClasses(outResources);
            if (expected != outClasses.size()) {
                throw new AssertionError("Classes were not added. Expected: " + expected
                        + ", got: " + outClasses.size());
            }
            for (ModuleEntry in : inClasses) {
                String path = in.getPath();
                if (!outClasses.contains(in)) {
                    throw new AssertionError("Class not found: " + path);
                }
                if (path.endsWith("module-info.class")) {
                    continue;
                }
                String modifiedPath = path.replace(".class", SUFFIX + ".class");
                if (!outClasses.contains(ModuleEntry.create(modifiedPath, new byte[0]))) {
                    throw new AssertionError("Class not found: " + modifiedPath);
                }
            }
        }
    }

    private class AddResourcesPlugin extends TestPlugin {

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            for (ModuleEntry res : globalPool.getResourceFiles()) {
                String path = res.getPath();
                String moduleName = getModule(path);
                AsmModulePool modulePool = pools.getModulePool(moduleName);
                WritableResourcePool resourcePool = modulePool.getTransformedResourceFiles();
                resourcePool.addResourceFile(new ResourceFile(removeModule(res.getPath()) + SUFFIX,
                        res.getBytes()));
            }
        }

        @Override
        public void test(ModulePool in, ModulePool out) throws Exception {
            Collection<ModuleEntry> inResources = extractResources(in);
            Collection<ModuleEntry> outResources = extractResources(out);
            if (2 * inResources.size() != outResources.size()) {
                throw new AssertionError("Classes were not added. Expected: " + (2 * inResources.size())
                        + ", got: " + outResources.size());
            }
            for (ModuleEntry r : inResources) {
                String path = r.getPath();
                if (!outResources.contains(r)) {
                    throw new AssertionError("Class not found: " + path);
                }
                String modifiedPath = path + SUFFIX;
                if (!outResources.contains(ModuleEntry.create(modifiedPath, new byte[0]))) {
                    throw new AssertionError("Class not found: " + modifiedPath);
                }
            }
        }
    }

    private class ReplaceClassesPlugin extends TestPlugin {

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            WritableClassPool transformedClasses = globalPool.getTransformedClasses();
            for (ModuleEntry res : globalPool.getClasses()) {
                ClassReader reader = globalPool.getClassReader(res);
                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
                reader.accept(new AddMethodClassVisitor(writer), ClassReader.EXPAND_FRAMES);
                transformedClasses.addClass(writer);
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool outResources) throws Exception {
            Collection<ModuleEntry> inClasses = extractClasses(inResources);
            Collection<ModuleEntry> outClasses = extractClasses(outResources);
            if (inClasses.size() != outClasses.size()) {
                throw new AssertionError("Number of classes. Expected: " + (inClasses.size())
                        + ", got: " + outClasses.size());
            }
            for (ModuleEntry out : outClasses) {
                String path = out.getPath();
                if (!inClasses.contains(out)) {
                    throw new AssertionError("Class not found: " + path);
                }
                ClassFile cf = ClassFile.read(new ByteArrayInputStream(out.getBytes()));
                if (path.endsWith("module-info.class")) {
                    continue;
                }
                boolean failed = true;
                for (Method m : cf.methods) {
                    if (m.getName(cf.constant_pool).equals(SUFFIX)) {
                        failed = false;
                    }
                }
                if (failed) {
                    throw new AssertionError("Not found method with name " + SUFFIX + " in class " + path);
                }
            }
        }
    }

    private class ReplaceResourcesPlugin extends TestPlugin {

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            for (ModuleEntry res : globalPool.getResourceFiles()) {
                String path = res.getPath();
                AsmModulePool modulePool = pools.getModulePool(getModule(path));
                modulePool.getTransformedResourceFiles().addResourceFile(new ResourceFile(removeModule(path),
                        "HUI".getBytes()));
            }
        }

        @Override
        public void test(ModulePool in, ModulePool out) throws Exception {
            Collection<ModuleEntry> inResources = extractResources(in);
            Collection<ModuleEntry> outResources = extractResources(out);
            if (inResources.size() != outResources.size()) {
                throw new AssertionError("Number of resources. Expected: " + inResources.size()
                        + ", got: " + outResources.size());
            }
            for (ModuleEntry r : outResources) {
                String path = r.getPath();
                if (!inResources.contains(r)) {
                    throw new AssertionError("Resource not found: " + path);
                }
                String content = new String(r.getBytes());
                if (!"HUI".equals(content)) {
                    throw new AssertionError("Content expected: 'HUI', got: " + content);
                }
            }
        }
    }

    private class ForgetClassesPlugin extends TestPlugin {

        private int expected = 0;

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            WritableClassPool transformedClasses = globalPool.getTransformedClasses();
            int i = 0;
            for (ModuleEntry res : globalPool.getClasses()) {
                String path = removeModule(res.getPath());
                String className = path.replace(".class", "");
                if ((i & 1) == 0 && !className.endsWith("module-info")) {
                    transformedClasses.forgetClass(className);
                } else {
                    ++expected;
                }
                i ^= 1;
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool outResources) throws Exception {
            Collection<ModuleEntry> outClasses = extractClasses(outResources);
            if (expected != outClasses.size()) {
                throw new AssertionError("Number of classes. Expected: " + expected +
                        ", got: " + outClasses.size());
            }
        }
    }

    private class ForgetResourcesPlugin extends TestPlugin {

        private int expectedAmount = 0;

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            int i = 0;
            for (ModuleEntry res : globalPool.getResourceFiles()) {
                String path = res.getPath();
                if (!path.contains("META-INF/services")) {
                    if ((i & 1) == 0) {
                        AsmModulePool modulePool = pools.getModulePool(getModule(path));
                        modulePool.getTransformedResourceFiles().forgetResourceFile(removeModule(res.getPath()));
                    } else {
                        ++expectedAmount;
                    }
                    i ^= 1;
                } else {
                    ++expectedAmount;
                }
            }
        }

        @Override
        public void test(ModulePool in, ModulePool out) throws Exception {
            Collection<ModuleEntry> outResources = extractResources(out);
            if (expectedAmount != outResources.size()) {
                throw new AssertionError("Number of classes. Expected: " + expectedAmount
                        + ", got: " + outResources.size());
            }
        }
    }

    private class AddForgetClassesPlugin extends TestPlugin {

        private int expected = 0;

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            WritableClassPool transformedClasses = globalPool.getTransformedClasses();
            int i = 0;
            for (ModuleEntry res : globalPool.getClasses()) {
                ClassReader reader = globalPool.getClassReader(res);
                String className = reader.getClassName();
                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
                if (!className.endsWith("module-info")) {
                    reader.accept(new RenameClassVisitor(writer), ClassReader.EXPAND_FRAMES);
                    transformedClasses.addClass(writer);
                    ++expected;
                }

                if ((i & 1) == 0 && !className.endsWith("module-info")) {
                    transformedClasses.forgetClass(className);
                } else {
                    ++expected;
                }
                i ^= 1;
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool outResources) throws Exception {
            Collection<ModuleEntry> outClasses = extractClasses(outResources);
            if (expected != outClasses.size()) {
                throw new AssertionError("Number of classes. Expected: " + expected
                        + ", got: " + outClasses.size());
            }
        }
    }

    private class AddForgetResourcesPlugin extends TestPlugin {

        private int expectedAmount = 0;

        @Override
        public void visit() {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            int i = 0;
            for (ModuleEntry res : globalPool.getResourceFiles()) {
                String path = res.getPath();
                String moduleName = getModule(path);
                if (!path.contains("META-INF")) {
                    AsmModulePool modulePool = pools.getModulePool(moduleName);
                    WritableResourcePool transformedResourceFiles = modulePool.getTransformedResourceFiles();
                    String newPath = removeModule(path) + SUFFIX;
                    transformedResourceFiles.addResourceFile(new ResourceFile(newPath, res.getBytes()));
                    if ((i & 1) == 0) {
                        transformedResourceFiles.forgetResourceFile(newPath);
                    } else {
                        ++expectedAmount;
                    }
                    i ^= 1;
                }
                ++expectedAmount;
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool out) throws Exception {
            Collection<ModuleEntry> outResources = extractResources(out);
            if (expectedAmount != outResources.size()) {
                throw new AssertionError("Number of classes. Expected: " + expectedAmount
                        + ", got: " + outResources.size());
            }
        }
    }

    private class ComboPlugin extends TestPlugin {

        private class RenameClassVisitor extends ClassVisitor {

            public RenameClassVisitor(ClassWriter cv) {
                super(Opcodes.ASM5, cv);
            }

            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, access, name + SUFFIX, signature, superName, interfaces);
            }
        }

        @Override
        public void visit() {
            try {
                renameClasses();
                renameResources();
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }

        @Override
        public void test(ModulePool inResources, ModulePool outResources) throws Exception {
            if (!isVisitCalled()) {
                throw new AssertionError("Resources not visited");
            }
            AsmGlobalPool globalPool = getPools().getGlobalPool();
            if (globalPool.getTransformedClasses().getClasses().size() != getClasses().size()) {
                throw new AssertionError("Number of transformed classes not equal to expected");
            }
            // Check that only renamed classes and resource files are in the result.
            outResources.entries().forEach(r -> {
                String resourceName = r.getPath();
                if (resourceName.endsWith(".class") && !resourceName.endsWith("module-info.class")) {
                    if (!resourceName.endsWith(SUFFIX + ".class")) {
                        throw new AssertionError("Class not renamed " + resourceName);
                    }
                } else if (resourceName.contains("META-INF/services/") && MODULES.containsKey(r.getModule())) {
                    String newClassName = new String(r.getBytes());
                    if(!newClassName.endsWith(SUFFIX)) {
                        throw new AssertionError("Resource file not renamed " + resourceName);
                    }
                }
            });
        }

        private void renameResources() throws IOException {
            AsmPools pools = getPools();
            // Rename the resource Files
            for (Map.Entry<String, List<String>> mod : MODULES.entrySet()) {
                String moduleName = mod.getKey();
                AsmModulePool modulePool = pools.getModulePool(moduleName);
                for (ModuleEntry res : modulePool.getResourceFiles()) {
                    ResourceFile resFile = modulePool.getResourceFile(res);
                    if (resFile.getPath().startsWith("META-INF/services/")) {
                        String newContent = new String(resFile.getContent()) + SUFFIX;
                        ResourceFile newResourceFile = new ResourceFile(resFile.getPath(),
                                newContent.getBytes());
                        modulePool.getTransformedResourceFiles().addResourceFile(newResourceFile);
                    }
                }
            }
        }

        private void renameClasses() throws IOException {
            AsmPools pools = getPools();
            AsmGlobalPool globalPool = pools.getGlobalPool();
            WritableClassPool transformedClasses = globalPool.getTransformedClasses();
            for (ModuleEntry res : globalPool.getClasses()) {
                if (res.getPath().endsWith("module-info.class")) {
                    continue;
                }
                ClassReader reader = globalPool.getClassReader(res);
                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
                RenameClassVisitor visitor = new RenameClassVisitor(writer);
                reader.accept(visitor, ClassReader.EXPAND_FRAMES);

                transformedClasses.forgetClass(reader.getClassName());
                transformedClasses.addClass(writer);
            }
        }
    }
}