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

package vm.runtime.defmeth.shared;

import vm.runtime.defmeth.shared.data.Clazz;
import java.io.PrintWriter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nsk.share.Pair;
import nsk.share.TestFailure;
import vm.runtime.defmeth.shared.data.method.param.*;


/**
 * Utility class with auxiliary miscellaneous methods.
 */
public class Util {
    public static class Transformer {
        private static Instrumentation inst;

        public static void premain(String agentArgs, Instrumentation inst) {
            Transformer.inst = inst;

            /*
            inst.addTransformer(new ClassFileTransformer() {
                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                    System.out.println("Retransform (initial): " + className);
                    return classfileBuffer;
                }
            });
            */
        }
    }

    /**
     * Concatenate {@code strings} array interleaving with {@code sep} in between.
     *
     * @param sep
     * @param strings
     * @return
     */
    public static String intersperse(String sep, String... strings) {
        StringBuilder sb = new StringBuilder();
        if (strings.length == 0) {
            return "";
        } else if (strings.length == 1) {
            return strings[0];
        } else {
            sb.append(strings[0]);

            for (int i=1; i<strings.length; i++) {
                sb.append(sep).append(strings[i]);
            }

            return sb.toString();
        }
    }

    /**
     * Construct array of names for an array of {@code Clazz} instances.
     *
     * @param clazzes
     * @return
     */
    public static String[] asStrings(Clazz[] clazzes) {
        String[] result = new String[clazzes.length];
        for (int i = 0; i < clazzes.length; i++) {
            result[i] = clazzes[i].intlName();
        }

        return result;
    }

    /**
     * Get the name of the test currently being executed
     *
     * @return name of the test being executed
     */
    public static String getTestName() {
        // Hack: examine stack trace and extract test method's name from there
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement elem : e.getStackTrace()) {
                String className = elem.getClassName();
                String methodName = elem.getMethodName();

                if (className.startsWith("vm.runtime.defmeth.") &&
                        methodName.startsWith("test")) {
                    return String.format("%s.%s",
                            className.replaceAll(".*\\.", ""), methodName);
                }
            }

            return "Unknown";
        }
    }

    /**
     * Pretty-print {@code byte[] classFile} to stdout.
     *
     * @param classFile
     */
    public static void printClassFile(byte[] classFile) {
        int flags =  jdk.internal.org.objectweb.asm.ClassReader.SKIP_DEBUG;

        classFile = classFile.clone();

        // FIXME: workaround for V1_7 limiation of current ASM
        // Artificially downgrade
        if (classFile[7] == 52) {
            System.out.println("WARNING: downgraded major verson from 52 to 0");
            classFile[7] = 0;
        }

         jdk.internal.org.objectweb.asm.ClassReader cr =
                new  jdk.internal.org.objectweb.asm.ClassReader(classFile);

        cr.accept(new  jdk.internal.org.objectweb.asm.util.TraceClassVisitor(new PrintWriter(System.out)),
                flags);

    }

    /**
     * Print ASM version (sequence of calls to ASM API to produce same class file)
     * of {@code classFile} to stdout.
     *
     * @param classFile
     */
    public static void asmifyClassFile(byte[] classFile) {
        int flags =  jdk.internal.org.objectweb.asm.ClassReader.SKIP_DEBUG;

        // FIXME: workaround for V1_7 limiation of current ASM
        // Artificially downgrade
        if (classFile[7] == 52) {
            // Need to patch the bytecode, so make a copy of the class file
            classFile = classFile.clone();

            System.out.println("WARNING: downgraded major verson from 52 to 0");
            classFile[7] = 0;
        }

         jdk.internal.org.objectweb.asm.ClassReader cr =
                new  jdk.internal.org.objectweb.asm.ClassReader(classFile);

        //cr.accept(new TraceClassVisitor(new PrintWriter(System.out)), flags);
        cr.accept(new  jdk.internal.org.objectweb.asm.util.TraceClassVisitor(null,
                        new  jdk.internal.org.objectweb.asm.util.ASMifier(),
                        new PrintWriter(System.out)), flags);
    }

    /**
     * Parse method descriptor and split it into parameter types names and
     * return type name.
     *
     * @param desc
     * @return {@code Pair} of parameter types names and
     */
    public static Pair<String[],String> parseDesc(String desc) {
        Pattern p = Pattern.compile("\\((.*)\\)(.*)");
        Matcher m = p.matcher(desc);

        if (m.matches()) {
            String opts = m.group(1);
            String returnVal = m.group(2);

            return Pair.of(parseParams(opts), returnVal);
        } else {
            throw new IllegalArgumentException(desc);
        }

    }

    /**
     * Check whether a type isn't Void by it's name.
     *
     * @param type return type name
     * @return
     */
    public static boolean isNonVoid(String type) {
        return !("V".equals(type));
    }

    /**
     * Split a sequence of type names (in VM internal form).
     *
     * Example:
     *   "BCD[[ALA;I" => [ "B", "C", "D", "[[A", "LA;", "I" ]
     *
     * @param str
     * @return
     */
    public static String[] parseParams(String str) {
        List<String> params = new ArrayList<>();

        /* VM basic type notation:
                B   byte    signed byte
                C   char    Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
                D   double  double-precision floating-point value
                F   float   single-precision floating-point value
                I   int     integer
                J   long    long integer
                L Classname ;   reference   an instance of class Classname
                S   short   signed short
                Z   boolean true or false
                [   reference   one array dimension
         */
        int i = 0;
        int start = 0;
        while (i < str.length()) {
            char c = str.charAt(i);
            switch (c) {
                case 'B': case 'C': case 'D': case 'F':
                case 'I': case 'J': case 'S': case 'Z':
                    params.add(str.substring(start, i+1));
                    start = i+1;
                    break;
                case 'L':
                    int k = str.indexOf(';', i);
                    if (k != 1) {
                        params.add(str.substring(start, k+1));
                        start = k+1;
                        i = k;
                    } else {
                        throw new IllegalArgumentException(str);
                    }
                    break;
                case '[':
                    break;
                default:
                    throw new IllegalArgumentException(
                            String.format("%d(%d): %c \'%s\'", i, start, c, str));
            }

            i++;
        }

        if (start != str.length()) {
            throw new IllegalArgumentException(str);
        }

        return params.toArray(new String[0]);
    }

    /**
     * Returns default values for different types:
     *   - byte:    0
     *   - short:   0
     *   - int:     0
     *   - long:    0L
     *   - char:    \U0000
     *   - boolean: false
     *   - float:   0.0f
     *   - double:  0.0d
     *   - array:   null
     *   - Object:  null
     *
     * @param types
     * @return
     */
    public static Param[] getDefaultValues(String[] types) {
        List<Param> values = new ArrayList<>();

        for (String type : types) {
            switch (type) {
                case "I":  case "B": case "C": case "Z": case "S":
                    values.add(new IntParam(0));
                    break;
                case "J":
                    values.add(new LongParam(0L));
                    break;
                case "D":
                    values.add(new DoubleParam(0.0d));
                    break;
                case "F":
                    values.add(new FloatParam(0.0f));
                    break;
                default:
                    if (type.startsWith("L") || type.startsWith("[")) {
                        values.add(new NullParam());
                    } else {
                        throw new IllegalArgumentException(Arrays.toString(types));
                    }
                    break;
            }
        }

        return values.toArray(new Param[0]);
    }

    /**
     * Decode class name from internal VM representation into normal Java name.
     * Internal class naming convention is extensively used to describe method type (descriptor).
     *
     * Examples:
     *    "Ljava/lang/Object" => "java.lang.Object"
     *    "I" => "int"
     *    "[[[C" => "char[][][]"
     *
     * @param name
     * @return
     */
    public static String decodeClassName(String name) {
        switch (name) {
            case "Z": return "boolean";
            case "B": return "byte";
            case "S": return "short";
            case "C": return "char";
            case "I": return "int";
            case "J": return "long";
            case "F": return "float";
            case "D": return "double";
            default:
                if (name.startsWith("L")) {
                    // "Ljava/lang/String;" => "java.lang.String"
                    return name.substring(1, name.length()-1).replaceAll("/", ".");
                } else if (name.startsWith("[")) {
                    // "[[[C" => "char[][][]"
                    return decodeClassName(name.substring(1)) + "[]";
                } else {
                    throw new IllegalArgumentException(name);
                }
        }
    }

    /**
     * Decode class name from internal VM format into regular name and resolve it using {@code cl} {@code ClassLoader}.
     * It is used during conversion of method type from string representation to strongly typed variants (e.g. MethodType).
     *
     * @param name
     * @param cl
     * @return
     */
    public static Class decodeClass(String name, ClassLoader cl) {
        switch (name) {
            case "Z": return boolean.class;
            case "B": return byte.class;
            case "S": return short.class;
            case "C": return char.class;
            case "I": return int.class;
            case "J": return long.class;
            case "F": return float.class;
            case "D": return double.class;
            case "V": return void.class;
            default:
                if (name.startsWith("L")) {
                    // "Ljava/lang/String;" => "java.lang.String"
                    String decodedName = name.substring(1, name.length()-1).replaceAll("/", ".");
                    try {
                        return cl.loadClass(decodedName);
                    } catch (Exception e) {
                        throw new Error(e);
                    }
                } else if (name.startsWith("[")) {
                    // "[[[C" => "char[][][]"
                    //return decodeClassName(name.substring(1)) + "[]";
                    throw new UnsupportedOperationException("Resolution of arrays isn't supported yet: "+name);
                } else {
                    throw new IllegalArgumentException(name);
                }
        }
    }

    /**
     * Redefine a class with a new version.
     *
     * @param clz class for redefinition
     */
    static public void retransformClass(final Class<?> clz, final byte[] classFile) {
        ClassFileTransformer transformer = new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (clz.getClassLoader() == loader && className.equals(clz.getName())) {
                    if (Constants.TRACE_CLASS_REDEF)  System.out.println("RETRANSFORM: " + className);

                    return classFile;
                } else {
                    // leave the class as-is
                    return classfileBuffer;
                }
            }
        };

        Transformer.inst.addTransformer(transformer, true);
        try {
            Transformer.inst.retransformClasses(clz);
        } catch (UnmodifiableClassException e) {
            throw new TestFailure(e);
        } finally {
            Transformer.inst.removeTransformer(transformer);
        }
    }

    /**
     * Redefine a class with a new version (class file in byte array).
     *
     * @param clz class for redefinition
     * @param classFile new version as a byte array
     * @return false if any errors occurred during class redefinition
     */
    static public void redefineClass(Class<?> clz, byte[] classFile) {
        if (clz ==  null) {
            throw new IllegalArgumentException("clz == null");
        }

        if (classFile == null || classFile.length == 0) {
            throw new IllegalArgumentException("Incorrect classFile");
        }

        if (Constants.TRACE_CLASS_REDEF)  System.out.println("REDEFINE: "+clz.getName());

        if (!redefineClassIntl(clz, classFile)) {
            throw new TestFailure("redefineClass failed: "+clz.getName());
        }
    }


    native static public boolean redefineClassIntl(Class<?> clz, byte[] classFile);

    /**
     * Get VM internal name of {@code Class<?> clz}.
     *
     * @param clz
     * @return
     */
    public static String getInternalName(Class<?> clz) {
        if (!clz.isPrimitive()) {
            return clz.getName().replaceAll("\\.", "/");
        } else {
            throw new UnsupportedOperationException();
        }
    }
}
