package org.eclipse.emt4j.analysis.common.util;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.emt4j.common.DependTarget;
import org.objectweb.asm.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static org.objectweb.asm.Opcodes.ASM9;

public class DeprecateAPIScanTool {
    private static final String DESC_DEPRECATED = "Ljava/lang/Deprecated;";

    public static void main(String[] args) throws IOException {
        List<Path> files = walk(new File(args[0]));
        List<String> results = new ArrayList<>();
        results.add("# Generated by org.eclipse.emt4j.analysis.common.util.DeprecateAPIScanTool at  " + new Date());
        Consumer<byte[]> consumer = new FindDeprecatedAPI(new DeprecatedAPIConsumer() {
            @Override
            public void onClassDeprecated(String className) {
                if (!"module-info".equals(className)) {
                    results.add(String.join(",", normalize(className), DependTarget.Method.ANY_DESC, DependTarget.Method.ANY_DESC));
                }
            }

            @Override
            public void onMethodDeprecated(String className, String methodName, String methodDesc) {
                results.add(String.join(",", normalize(className), methodName, methodDesc));
            }
        });
        for (Path file : files) {
            String fileStr = file.toString().toLowerCase();
            if (fileStr.endsWith(".jar")) {
                analyzeJar(file, consumer);
            } else if (fileStr.endsWith(".class")) {
                analyzeClass(file, consumer);
            }
        }
        results.sort(Comparator.naturalOrder());
        FileUtils.writeLines(new File("depcated_api.cfg"), results);
    }

    private interface DeprecatedAPIConsumer {
        void onClassDeprecated(String className);

        void onMethodDeprecated(String className, String methodName, String methodDesc);
    }

    public static void analyzeJar(Path jarFilePath, Consumer<byte[]> consumer) throws IOException {
        JarFile jarFile = new JarFile(jarFilePath.toFile());
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            if (jarEntry.getName().endsWith(".class")) {
                try (InputStream input = jarFile.getInputStream(jarEntry)) {
                    byte[] classFileContent = IOUtils.toByteArray(input);
                    consumer.accept(classFileContent);
                } catch (Exception e) {
                    // we don't want an error interrupt the analysis process
                    e.printStackTrace();
                }
            }
        }
    }

    private static class FindDeprecatedAPI implements Consumer<byte[]> {
        private final DeprecatedAPIConsumer deprecatedAPIConsumer;

        public FindDeprecatedAPI(DeprecatedAPIConsumer deprecatedAPIConsumer) {
            this.deprecatedAPIConsumer = deprecatedAPIConsumer;
        }

        @Override
        public void accept(byte[] bytes) {
            ClassReader cr = new ClassReader(bytes);
            cr.accept(new FindDeprecatedClassVisitor(ASM9, deprecatedAPIConsumer), 0);
        }
    }

    private static class FindDeprecatedClassVisitor extends ClassVisitor {

        private final DeprecatedAPIConsumer deprecatedAPIConsumer;
        private String className;

        public FindDeprecatedClassVisitor(int api, DeprecatedAPIConsumer deprecatedAPIConsumer) {
            super(api);
            this.deprecatedAPIConsumer = deprecatedAPIConsumer;
        }

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

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            if (DESC_DEPRECATED.equals(descriptor)) {
                deprecatedAPIConsumer.onClassDeprecated(className);
            }
            return super.visitAnnotation(descriptor, visible);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            return new FindDeprecatedMethodVisitor(ASM9, deprecatedAPIConsumer, className, name, descriptor);
        }
    }

    private static class FindDeprecatedMethodVisitor extends MethodVisitor {

        private final DeprecatedAPIConsumer deprecatedAPIConsumer;
        private final String className;
        private final String methodName;
        private final String methodDesc;

        public FindDeprecatedMethodVisitor(int api, DeprecatedAPIConsumer deprecatedAPIConsumer, String className, String methodName, String methodDesc) {
            super(api);
            this.deprecatedAPIConsumer = deprecatedAPIConsumer;
            this.className = className;
            this.methodName = methodName;
            this.methodDesc = methodDesc;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            if (DESC_DEPRECATED.equals(descriptor)) {
                deprecatedAPIConsumer.onMethodDeprecated(className, methodName, methodDesc);
            }
            return super.visitAnnotation(descriptor, visible);
        }
    }

    public static void analyzeClass(Path classFilePath, Consumer<byte[]> consumer) throws IOException {
        try (InputStream inputStream = new FileInputStream(classFilePath.toFile())) {
            byte[] classFileContent = IOUtils.toByteArray(inputStream);
            consumer.accept(classFileContent);
        }
    }

    private static List<Path> walk(File directory) throws IOException {
        List<Path> candidateFiles = new ArrayList<>();
        Files.walkFileTree(directory.toPath(), new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                String fileStr = file.toString().toLowerCase();
                if (fileStr.endsWith(".jar") || fileStr.endsWith(".class")) {
                    candidateFiles.add(file);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                return FileVisitResult.CONTINUE;
            }
        });
        return candidateFiles;
    }

    private static String normalize(String internal) {
        if (null != internal) {
            return internal.replace('/', '.').replace('$', '.');
        } else {
            return null;
        }
    }
}
