/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2018 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.jboss.as.hibernate;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.logging.BasicLogger;
import org.jboss.logging.Logger;
import org.jboss.modules.ModuleClassLoader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.util.TraceClassVisitor;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * A class file transformer which makes deployment classes written for Hibernate 5.1 be compatible with Hibernate 5.3.
 *
 * @deprecated
 */
public class Hibernate51CompatibilityTransformer implements ClassFileTransformer {

    private static final Hibernate51CompatibilityTransformer instance = new Hibernate51CompatibilityTransformer();
    // TODO: replace useASMExperimental and use of Opcodes.ASM7_EXPERIMENTAL with Opcodes.ASM7 when ASM JDK 11 support is available.
    private static final File showTransformedClassFolder;
    public static final BasicLogger logger = Logger.getLogger("org.jboss.as.hibernate.transformer");

    static {
        String folderName = WildFlySecurityManager.getPropertyPrivileged("Hibernate51CompatibilityTransformer.showTransformedClassFolder", null);
        if (folderName != null) {
            showTransformedClassFolder = new File(folderName);
        } else {
            showTransformedClassFolder = null;
        }
    }

    private static final boolean useASMExperimental = getMajorJavaVersion() >= 11;

    private static final String markerAlreadyTransformed = "$_org_jboss_as_hibernate_Hibernate51CompatibilityTransformer_transformed_$";

    public static Hibernate51CompatibilityTransformer getInstance() {
        return instance;
    }

    @Override
    public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
        logger.debugf("Hibernate51CompatibilityTransformer transforming deployment class '%s' from '%s'", className, getModuleName(loader));

        final Set<String> parentClassesAndInterfaces = new HashSet<>();
        collectClassesAndInterfaces(parentClassesAndInterfaces, loader, className);
        logger.tracef("Class %s extends or implements %s", className, parentClassesAndInterfaces);

        final TransformedState transformedState = new TransformedState();
        final ClassReader classReader = new ClassReader(classfileBuffer);
        final ClassWriter classWriter = new ClassWriter(classReader, 0);
        ClassVisitor traceClassVisitor = classWriter;
        PrintWriter tracePrintWriter = null;
        try {
            if (showTransformedClassFolder != null) {
                tracePrintWriter = new PrintWriter(new File(showTransformedClassFolder, className.replace('/', '_') + ".asm"));
                traceClassVisitor = new TraceClassVisitor(classWriter, tracePrintWriter);
            }
        } catch (IOException ignored) {

        }
        try {
            classReader.accept(new ClassVisitor(useASMExperimental ? Opcodes.ASM7_EXPERIMENTAL : Opcodes.ASM6, traceClassVisitor) {

                // clear transformed state at start of each class visit
                @Override
                public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                    // clear per class state
                    transformedState.clear();
                    super.visit(version, access, name, signature, superName, interfaces);
                }

                // check if class has already been transformed
                @Override
                public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                    // check if class has already been modified
                    if (markerAlreadyTransformed.equals(name) &&
                            desc.equals("Z")) {
                        transformedState.setAlreadyTransformed(true);
                    }
                    return super.visitField(access, name, desc, signature, value);
                }

                // mark class as transformed (only if class transformations were made)
                @Override
                public void visitEnd() {
                    if (transformedState.transformationsMade()) {
                        cv.visitField(ACC_PUBLIC + ACC_STATIC, markerAlreadyTransformed, "Z", null, null).visitEnd();
                    }
                    super.visitEnd();
                }

                @Override
                public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

                    // Handle changing SessionImplementor parameter to SharedSessionContractImplementor
                    boolean rewriteSessionImplementor = false;
                    final String descOrig = desc;

                    logger.tracef("method %s, description %s, signature %s", name, desc, signature);
                    if (parentClassesAndInterfaces.contains("org/hibernate/usertype/UserType")) {
                        if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;[Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeSet") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;ILorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/usertype/CompositeUserType")) {
                        if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;[Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeSet") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;ILorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("assemble") &&
                                "(Ljava/io/Serializable;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("disassemble") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/io/Serializable;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("replace") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }
                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/usertype/UserCollectionType")) {
                        if (name.equals("instantiate") &&
                                "(Lorg/hibernate/engine/spi/SessionImplementor;Lorg/hibernate/persister/collection/CollectionPersister;)Lorg/hibernate/collection/spi/PersistentCollection;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("replaceElements") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;Lorg/hibernate/persister/collection/CollectionPersister;Ljava/lang/Object;Ljava/util/Map;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("wrap") &&
                                "(Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Lorg/hibernate/collection/spi/PersistentCollection;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/usertype/UserVersionType")) {
                        if (name.equals("seed") &&
                                "(Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("next") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/Type")) {
                        if (name.equals("assemble") &&
                                "(Ljava/io/Serializable;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("disassemble") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/io/Serializable;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("beforeAssemble") &&
                                "(Ljava/io/Serializable;Lorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("hydrate") &&
                                "(Ljava/sql/ResultSet;[Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("isDirty") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;)Z".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("isDirty") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;[ZLorg/hibernate/engine/spi/SessionImplementor;)Z".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("isModified") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;[ZLorg/hibernate/engine/spi/SessionImplementor;)Z".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;[Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeSet") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;I[ZLorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeSet") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;ILorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("replace") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;Ljava/util/Map;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("replace") &&
                                "(Ljava/lang/Object;Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;Ljava/util/Map;Lorg/hibernate/type/ForeignKeyDirection;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("resolve") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("resolve") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;Ljava/lang/Boolean;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("semiResolve") &&
                                "(Ljava/lang/Object;Lorg/hibernate/engine/spi/SessionImplementor;Ljava/lang/Object;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/SingleColumnType")) {
                        if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("get") &&
                                "(Ljava/sql/ResultSet;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("set") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;ILorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/AbstractStandardBasicType")) {
                        if (name.equals("get") &&
                                "(Ljava/sql/ResultSet;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("nullSafeGet") &&
                                "(Ljava/sql/ResultSet;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)Ljava/lang/Object;".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("set") &&
                                "(Ljava/sql/PreparedStatement;Ljava/lang/Object;ILorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/ProcedureParameterExtractionAware")) {
                        if (name.equals("extract")
                                && (desc.startsWith(
                                "(Ljava/sql/CallableStatement;ILorg/hibernate/engine/spi/SessionImplementor;)")
                                || desc.startsWith(
                                "(Ljava/sql/CallableStatement;[Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)"))) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/ProcedureParameterNamedBinder")) {
                        if (name.equals("nullSafeSet") &&
                                "(Ljava/sql/CallableStatement;Ljava/lang/Object;Ljava/lang/String;Lorg/hibernate/engine/spi/SessionImplementor;)V".equals(desc)) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/type/VersionType")) {
                        if (name.equals("seed") &&
                                desc.startsWith("(Lorg/hibernate/engine/spi/SessionImplementor;)")) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("next") &&
                                desc.contains("Lorg/hibernate/engine/spi/SessionImplementor;")) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (parentClassesAndInterfaces.contains("org/hibernate/collection/spi/PersistentCollection")) {
                        if (name.equals("unsetSession") &&
                                desc.equals("(Lorg/hibernate/engine/spi/SessionImplementor;)Z")) {
                            desc = replaceSessionImplementor(desc);
                        } else if (name.equals("setCurrentSession") &&
                                desc.equals("(Lorg/hibernate/engine/spi/SessionImplementor;)Z")) {
                            desc = replaceSessionImplementor(desc);
                        }

                        rewriteSessionImplementor = true;
                    }

                    if (rewriteSessionImplementor && name.equals("<init>")) {
                        // update constructor methods to use SharedSessionContractImplementor instead of SessionImplementor
                        desc = replaceSessionImplementor(desc);
                    }

                    if (descOrig != desc) {  // if we are changing from type SessionImplementor to SharedSessionContractImplementor
                        // mark the class as transformed
                        transformedState.setClassTransformed(true);
                    }
                    return new MethodAdapter(rewriteSessionImplementor, Opcodes.ASM6, super.visitMethod(access, name, desc,
                            signature, exceptions), getModuleName(loader), className, transformedState);
                }
            }, 0);
            if (!transformedState.transformationsMade()) {
                // no change was actually made, indicate so by returning null
                return null;
            }
            return classWriter.toByteArray();
        } finally {
            if (tracePrintWriter != null) {
                tracePrintWriter.close();
            }
        }
    }

    private static String getModuleName(ClassLoader loader) {
        if (loader == null) {
            return "(null)";
        }
        if (loader instanceof ModuleClassLoader) {
            return ((ModuleClassLoader) loader).getName();
        }
        return loader.toString();
    }

    private static String replaceSessionImplementor(String desc) {
        return desc.replace("Lorg/hibernate/engine/spi/SessionImplementor;",
                "Lorg/hibernate/engine/spi/SharedSessionContractImplementor;");
    }

    private void collectClassesAndInterfaces(Set<String> classesAndInterfaces, ClassLoader classLoader, String className) {
        if (className == null || "java/lang/Object".equals(className)) {
            return;
        }

        try (InputStream is = classLoader.getResourceAsStream(className.replace('.', '/') + ".class")) {
            ClassReader classReader = new ClassReader(is);
            classReader.accept(new ClassVisitor(useASMExperimental ? Opcodes.ASM7_EXPERIMENTAL : Opcodes.ASM6) {

                @Override
                public void visit(int version, int access, String name, String signature,
                                  String superName, String[] interfaces) {
                    if (interfaces != null) {
                        for (String interfaceName : interfaces) {
                            classesAndInterfaces.add(interfaceName);
                            collectClassesAndInterfaces(classesAndInterfaces, classLoader, interfaceName);
                        }
                    }

                    classesAndInterfaces.add(superName);
                    collectClassesAndInterfaces(classesAndInterfaces, classLoader, superName);
                }

                @Override
                public void visitInnerClass(String name, String outerName, String innerName, int access) {
                    if (innerName != null) {
                        classesAndInterfaces.add(innerName);
                    }
                }

            }, 0);
        } catch (IOException e) {
            logger.warn("Unable to open class file %1$s", className, e);
        }
    }

    private static int getMajorJavaVersion() {
        int major = 8;
        String version = WildFlySecurityManager.getPropertyPrivileged("java.specification.version", null);
        if (version != null) {
            Matcher matcher = Pattern.compile("^(?:1\\.)?(\\d+)$").matcher(version);
            if (matcher.find()) {
                major = Integer.valueOf(matcher.group(1));
            }
        }
        return major;
    }
}

