/*
 * 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.apache.felix.scr.impl;

import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;

import junit.framework.TestCase;

/**
 * Ensure that the helper / manager / metadata packages can actually be used
 * without any other packages.
 */
public class PackageIsolationTest extends TestCase {

    public void testIsolation() throws Exception {
        final List<String> packages = Arrays.asList(
                "org/apache/felix/scr/component",
                "org/apache/felix/scr/impl/helper",
                "org/apache/felix/scr/impl/inject",
                "org/apache/felix/scr/impl/inject/field",
                "org/apache/felix/scr/impl/inject/methods",
                "org/apache/felix/scr/impl/logger",
                "org/apache/felix/scr/impl/manager",
                "org/apache/felix/scr/impl/metadata");


        StringBuilder regexp = new StringBuilder();
        regexp.append("target/classes/(");
        boolean first = true;
        for (String pkg : packages) {
            if (!first) {
                regexp.append("|");
            }
            regexp.append(pkg);
            first = false;
        }
        regexp.append(")/[A-Za-z]+\\.class");
        Pattern pattern = Pattern.compile(regexp.toString());

        DependencyVisitor v = new DependencyVisitor();
        check(new File("target/classes"), pattern, v);

        for (String pkg : v.getPackages()) {
            if (pkg.startsWith("org/apache/felix/scr")
                    && !packages.contains(pkg)) {
                fail("Unexpected dependency on package " + pkg + " found");
            }
        }
    }

    static void check(File file, Pattern regexp, ClassVisitor v) throws Exception {
        File[] children = file.listFiles();
        if (children != null) {
            for (File child : children) {
                if (regexp.matcher(child.toString()).matches()) {
                    new ClassReader(new FileInputStream(child)).accept(v, 0);
                }
                check(child, regexp, v);
            }
        }
    }

    public static class DependencyVisitor extends ClassVisitor {
        Set<String> packages = new HashSet<>();

        Map<String, Map<String, Integer>> groups = new HashMap<>();

        Map<String, Integer> current;

        public Map<String, Map<String, Integer>> getGlobals() {
            return groups;
        }

        public Set<String> getPackages() {
            return packages;
        }

        public DependencyVisitor() {
            super(Opcodes.ASM5);
        }

        // ClassVisitor

        @Override
        public void visit(final int version, final int access, final String name,
                          final String signature, final String superName,
                          final String[] interfaces) {
            String p = getGroupKey(name);
            current = groups.get(p);
            if (current == null) {
                current = new HashMap<>();
                groups.put(p, current);
            }

            if (signature == null) {
                if (superName != null) {
                    addInternalName(superName);
                }
                addInternalNames(interfaces);
            } else {
                addSignature(signature);
            }
        }

        @Override
        public AnnotationVisitor visitAnnotation(final String desc,
                                                 final boolean visible) {
            addDesc(desc);
            return new AnnotationDependencyVisitor();
        }

        @Override
        public AnnotationVisitor visitTypeAnnotation(final int typeRef,
                                                     final TypePath typePath, final String desc, final boolean visible) {
            addDesc(desc);
            return new AnnotationDependencyVisitor();
        }

        @Override
        public FieldVisitor visitField(final int access, final String name,
                                       final String desc, final String signature, final Object value) {
            if (signature == null) {
                addDesc(desc);
            } else {
                addTypeSignature(signature);
            }
            if (value instanceof Type) {
                addType((Type) value);
            }
            return new FieldDependencyVisitor();
        }

        @Override
        public MethodVisitor visitMethod(final int access, final String name,
                                         final String desc, final String signature, final String[] exceptions) {
            if (signature == null) {
                addMethodDesc(desc);
            } else {
                addSignature(signature);
            }
            addInternalNames(exceptions);
            return new MethodDependencyVisitor();
        }

        class AnnotationDependencyVisitor extends AnnotationVisitor {

            public AnnotationDependencyVisitor() {
                super(Opcodes.ASM5);
            }

            @Override
            public void visit(final String name, final Object value) {
                if (value instanceof Type) {
                    addType((Type) value);
                }
            }

            @Override
            public void visitEnum(final String name, final String desc,
                                  final String value) {
                addDesc(desc);
            }

            @Override
            public AnnotationVisitor visitAnnotation(final String name,
                                                     final String desc) {
                addDesc(desc);
                return this;
            }

            @Override
            public AnnotationVisitor visitArray(final String name) {
                return this;
            }
        }

        class FieldDependencyVisitor extends FieldVisitor {

            public FieldDependencyVisitor() {
                super(Opcodes.ASM5);
            }

            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public AnnotationVisitor visitTypeAnnotation(final int typeRef,
                                                         final TypePath typePath, final String desc,
                                                         final boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }
        }

        class MethodDependencyVisitor extends MethodVisitor {

            public MethodDependencyVisitor() {
                super(Opcodes.ASM5);
            }

            @Override
            public AnnotationVisitor visitAnnotationDefault() {
                return new AnnotationDependencyVisitor();
            }

            @Override
            public AnnotationVisitor visitAnnotation(final String desc,
                                                     final boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public AnnotationVisitor visitTypeAnnotation(final int typeRef,
                                                         final TypePath typePath, final String desc,
                                                         final boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public AnnotationVisitor visitParameterAnnotation(final int parameter,
                                                              final String desc, final boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public void visitTypeInsn(final int opcode, final String type) {
                addType(Type.getObjectType(type));
            }

            @Override
            public void visitFieldInsn(final int opcode, final String owner,
                                       final String name, final String desc) {
                addInternalName(owner);
                addDesc(desc);
            }

            @Override
            public void visitMethodInsn(final int opcode, final String owner,
                                        final String name, final String desc, final boolean itf) {
                addInternalName(owner);
                addMethodDesc(desc);
            }

            @Override
            public void visitInvokeDynamicInsn(String name, String desc,
                                               Handle bsm, Object... bsmArgs) {
                addMethodDesc(desc);
                addConstant(bsm);
                for (int i = 0; i < bsmArgs.length; i++) {
                    addConstant(bsmArgs[i]);
                }
            }

            @Override
            public void visitLdcInsn(final Object cst) {
                addConstant(cst);
            }

            @Override
            public void visitMultiANewArrayInsn(final String desc, final int dims) {
                addDesc(desc);
            }

            @Override
            public AnnotationVisitor visitInsnAnnotation(int typeRef,
                                                         TypePath typePath, String desc, boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public void visitLocalVariable(final String name, final String desc,
                                           final String signature, final Label start, final Label end,
                                           final int index) {
                addTypeSignature(signature);
            }

            @Override
            public AnnotationVisitor visitLocalVariableAnnotation(int typeRef,
                                                                  TypePath typePath, Label[] start, Label[] end, int[] index,
                                                                  String desc, boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }

            @Override
            public void visitTryCatchBlock(final Label start, final Label end,
                                           final Label handler, final String type) {
                if (type != null) {
                    addInternalName(type);
                }
            }

            @Override
            public AnnotationVisitor visitTryCatchAnnotation(int typeRef,
                                                             TypePath typePath, String desc, boolean visible) {
                addDesc(desc);
                return new AnnotationDependencyVisitor();
            }
        }

        class SignatureDependencyVisitor extends SignatureVisitor {

            String signatureClassName;

            public SignatureDependencyVisitor() {
                super(Opcodes.ASM5);
            }

            @Override
            public void visitClassType(final String name) {
                signatureClassName = name;
                addInternalName(name);
            }

            @Override
            public void visitInnerClassType(final String name) {
                signatureClassName = signatureClassName + "$" + name;
                addInternalName(signatureClassName);
            }
        }

        // ---------------------------------------------

        private String getGroupKey(String name) {
            int n = name.lastIndexOf('/');
            if (n > -1) {
                name = name.substring(0, n);
            }
            packages.add(name);
            return name;
        }

        private void addName(final String name) {
            if (name == null) {
                return;
            }
            String p = getGroupKey(name);
            if (current.containsKey(p)) {
                current.put(p, current.get(p) + 1);
            } else {
                current.put(p, 1);
            }
        }

        void addInternalName(final String name) {
            addType(Type.getObjectType(name));
        }

        private void addInternalNames(final String[] names) {
            for (int i = 0; names != null && i < names.length; i++) {
                addInternalName(names[i]);
            }
        }

        void addDesc(final String desc) {
            addType(Type.getType(desc));
        }

        void addMethodDesc(final String desc) {
            addType(Type.getReturnType(desc));
            Type[] types = Type.getArgumentTypes(desc);
            for (int i = 0; i < types.length; i++) {
                addType(types[i]);
            }
        }

        void addType(final Type t) {
            switch (t.getSort()) {
                case Type.ARRAY:
                    addType(t.getElementType());
                    break;
                case Type.OBJECT:
                    addName(t.getInternalName());
                    break;
                case Type.METHOD:
                    addMethodDesc(t.getDescriptor());
                    break;
            }
        }

        private void addSignature(final String signature) {
            if (signature != null) {
                new SignatureReader(signature)
                        .accept(new SignatureDependencyVisitor());
            }
        }

        void addTypeSignature(final String signature) {
            if (signature != null) {
                new SignatureReader(signature)
                        .acceptType(new SignatureDependencyVisitor());
            }
        }

        void addConstant(final Object cst) {
            if (cst instanceof Type) {
                addType((Type) cst);
            } else if (cst instanceof Handle) {
                Handle h = (Handle) cst;
                addInternalName(h.getOwner());
                addMethodDesc(h.getDesc());
            }
        }
    }

}
