/***
 * ASM: a very small and fast Java bytecode manipulation framework
 * Copyright (c) 2000-2011 INRIA, France Telecom
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.objectweb.asm.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.GZIPInputStream;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * A command line tool to transform classes in order to make them compatible
 * with Java 1.5, and to check that they use only the JDK 1.5 API and JDK 1.5
 * classfile features.
 * 
 * @author Eric Bruneton
 * @author Eugene Kuleshov
 */
public class Retrofitter {

    /**
     * The fields and methods of the JDK 1.5 API. Each string has the form
     * "<owner> <name><descriptor>".
     */
    static final HashSet<String> API = new HashSet<String>();

    /**
     * The class hierarchy of the JDK 1.5 API. Maps each class name to the name
     * of its super class.
     */
    static final HashMap<String, String> HIERARCHY = new HashMap<String, String>();

    /**
     * Transforms the classes from a source directory into a destination
     * directory to make them compatible with the JDK 1.5, and checks that they
     * only use the JDK 1.5 API.
     * 
     * @param args
     *            First argument: name of the .txt.gz file specifying the JDK
     *            1.5 API. Second argument: source directory. Third argument:
     *            destination directory.
     * @throws IOException
     *             if a file can't be read or written.
     */
    public static void main(final String[] args) throws IOException {
        File api = new File(args[0]);
        InputStream inputStream = new GZIPInputStream(new FileInputStream(api));
        BufferedReader reader = new LineNumberReader(
                new InputStreamReader(inputStream));
        while (true) {
            String line = reader.readLine();
            if (line != null) {
                if (line.startsWith("class")) {
                    String className = line.substring(6, line.lastIndexOf(' '));
                    String superClassName = line
                            .substring(line.lastIndexOf(' ') + 1);
                    HIERARCHY.put(className, superClassName);
                } else {
                    API.add(line);
                }
            } else {
                break;
            }
        }

        File src = new File(args[1]);
        File dst = new File(args[2]);
        if (!retrofit(src, dst)) {
            System.exit(1);
        }
    }

    /**
     * Transforms the source class file, or if it is a directory, its files
     * (recursively), into the destination file or directory, in order to make
     * them compatible with the JDK 1.5.
     * 
     * @param src
     *            source file or directory
     * @param dst
     *            destination file or directory
     * @return true if all the source classes use only the JDK 1.5 API.
     * @throws IOException
     */
    static boolean retrofit(final File src, final File dst) throws IOException {
        if (src.isDirectory()) {
            boolean result = true;
            File[] files = src.listFiles();
            if (files == null) {
                throw new IOException("unable to read files of " + src);
            }
            for (int i = 0; i < files.length; ++i) {
                result &= retrofit(files[i], new File(dst, files[i].getName()));
            }
            return result;
        } else if (src.getName().endsWith(".class")) {
            if (!dst.exists() || dst.lastModified() < src.lastModified()) {
                ClassReader classReader = new ClassReader(
                        new FileInputStream(src));
                ClassWriter classWriter = new ClassWriter(0);
                // No actual retrofit to do since we compile with target=1.5.
                ClassVerifier classVerifier = new ClassVerifier(classWriter);
                classReader.accept(classVerifier, 0);
                if (!classVerifier.ok) {
                    return false;
                }

                if (!dst.getParentFile().exists()
                        && !dst.getParentFile().mkdirs()) {
                    throw new IOException(
                            "Cannot create directory " + dst.getParentFile());
                }
                OutputStream os = new FileOutputStream(dst);
                try {
                    os.write(classWriter.toByteArray());
                } finally {
                    os.close();
                }
            }
            return true;
        } else {
            return true;
        }
    }

    /**
     * A ClassVisitor checking that a class uses only JDK 1.5 class file
     * features and the JDK 1.5 API.
     */
    static class ClassVerifier extends ClassVisitor {

        /** The name of the visited class/ */
        String className;

        /** The name of the currently visited method. */
        String currentMethodName;

        /** Whether the class uses only JDK 1.5 class file features and APIs. */
        boolean ok;

        public ClassVerifier(ClassVisitor cv) {
            // Make sure use we don't use Java 9 or higher classfile features.
            // We also want to make sure we don't use Java 6, 7 or 8 classfile
            // features (invokedynamic), but this can't be done in the same way.
            // Instead, we use manual checks below.
            super(Opcodes.ASM4, cv);
            ok = true;
        }

        @Override
        public void visit(final int version, final int access,
                final String name, final String signature,
                final String superName, final String[] interfaces) {
            if ((version & 0xFFFF) > Opcodes.V1_5) {
                System.err.println(
                        "ERROR: " + name + " version is newer than 1.5");
                ok = false;
            }
            className = name;
            super.visit(version, access, name, signature, superName,
                    interfaces);
        }

        @Override
        public MethodVisitor visitMethod(final int access, final String name,
                final String desc, final String signature,
                final String[] exceptions) {
            currentMethodName = name + desc;
            MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                    exceptions);
            return new MethodVisitor(Opcodes.ASM4, mv) {
                @Override
                public void visitFieldInsn(final int opcode, final String owner,
                        final String name, final String desc) {
                    check(owner, name);
                    super.visitFieldInsn(opcode, owner, name, desc);
                }

                @Override
                public void visitMethodInsn(final int opcode,
                        final String owner, final String name,
                        final String desc, final boolean itf) {
                    check(owner, name + desc);
                    super.visitMethodInsn(opcode, owner, name, desc, itf);
                }

                @Override
                public void visitLdcInsn(Object cst) {
                    if (cst instanceof Type) {
                        int sort = ((Type) cst).getSort();
                        if (sort == Type.METHOD) {
                            System.err.println(
                                    "ERROR: ldc with a MethodType called in "
                                            + className + ' '
                                            + currentMethodName
                                            + " is not available in JDK 1.5");
                            ok = false;
                        }
                    } else if (cst instanceof Handle) {
                        System.err.println(
                                "ERROR: ldc with a MethodHandle called in "
                                        + className + ' ' + currentMethodName
                                        + " is not available in JDK 1.5");
                        ok = false;
                    }
                    super.visitLdcInsn(cst);
                }

                @Override
                public void visitInvokeDynamicInsn(String name, String desc,
                        Handle bsm, Object... bsmArgs) {
                    System.err.println("ERROR: invokedynamic called in "
                            + className + ' ' + currentMethodName
                            + " is not available in JDK 1.5");
                    ok = false;
                    super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
                }
            };
        }

        /**
         * Checks whether or not a field or method is defined in the JDK 1.5
         * API.
         * 
         * @param owner
         *            A class name.
         * @param member
         *            A field name or a method name and descriptor.
         */
        void check(String owner, String member) {
            if (owner.startsWith("java/")) {
                String o = owner;
                while (o != null) {
                    if (API.contains(o + ' ' + member)) {
                        return;
                    }
                    o = HIERARCHY.get(o);
                }
                System.err.println("ERROR: " + owner + ' ' + member
                        + " called in " + className + ' ' + currentMethodName
                        + " is not defined in the JDK 1.5 API");
                ok = false;
            }
        }
    }
}
