/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.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.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2008-2013 Charles Oliver Nutter <headius@headius.com>
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.anno;

import jnr.ffi.provider.jffi.SkinnyMethodAdapter;
import org.jruby.internal.runtime.methods.DescriptorInfo;
import org.jruby.runtime.Visibility;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;

import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
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.logging.Logger;

import static org.jruby.util.CodegenUtils.*;
import static org.objectweb.asm.Opcodes.*;

/**
 * Annotation processor for generating "populators" to bind native Java methods as Ruby methods, and
 * to gather a list of classes seen during compilation that should have their invokers regenerated.
 *
 * NOTE: This class must ONLY reference classes in the org.jruby.anno package, to avoid forcing
 * a transitive dependency on any runtime JRuby classes.
 */
@SupportedAnnotationTypes({"org.jruby.anno.JRubyMethod"})
public class IndyBinder extends AbstractProcessor {

    public static final String POPULATOR_SUFFIX = "$POPULATOR";
    public static final String SRC_GEN_DIR = "target/classes/org/jruby/gen/";
    public static final int CLASS = 1;
    public static final int BASEMETHOD = 3;
    public static final int MODULEMETHOD = 4;
    public static final int RUNTIME = 5;
    public static final int SINGLETONCLASS = 6;
    public static final int RUBYMODULE = 1;
    private final List<CharSequence> classNames = new ArrayList<CharSequence>();
    private SkinnyMethodAdapter mv;
    private static final boolean DEBUG = false;

    @Override
    public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment roundEnvironment) {
        for (TypeElement element : ElementFilter.typesIn(roundEnvironment.getRootElements())) {
            processType(element);
        }

        try {
            FileWriter fw = new FileWriter("target/generated-sources/annotated_classes.txt");
            for (CharSequence name : classNames) {
                fw.write(name.toString());
                fw.write('\n');
            }
            fw.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

    public void processType(TypeElement cd) {
        // process inner classes
        for (TypeElement innerType : ElementFilter.typesIn(cd.getEnclosedElements())) {
            processType(innerType);
        }

        try {
            String qualifiedName = cd.getQualifiedName().toString().replace('.', '$');

            // skip anything not related to jruby
            if (!qualifiedName.contains("org$jruby")) {
                return;
            }

            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

            cw.visitAnnotation(p(Generated.class), true);

            cw.visit(Opcodes.V1_7, ACC_PUBLIC, ("org.jruby.gen." + qualifiedName + POPULATOR_SUFFIX).replace('.', '/'), null, "org/jruby/anno/TypePopulator", null);

            mv = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "<init>", "()V", null, null);
            mv.start();
            mv.aload(0);
            mv.invokespecial("org/jruby/anno/TypePopulator", "<init>", "()V");
            mv.voidreturn();
            mv.end();

            mv = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "populate", "(Lorg/jruby/RubyModule;Ljava/lang/Class;)V", null, null);

            mv.start();

            if (DEBUG) {
                mv.ldc("Using pregenerated populator: " + qualifiedName + POPULATOR_SUFFIX);
                mv.aprintln();
            }

            // scan for meta, compat, etc to reduce findbugs complaints about "dead assignments"
            boolean hasAnno = false;
            boolean hasMeta = false;
            boolean hasModule = false;
            for (ExecutableElement method : ElementFilter.methodsIn(cd.getEnclosedElements())) {
                JRubyMethod anno = method.getAnnotation(JRubyMethod.class);
                if (anno == null) {
                    continue;
                }
                hasAnno = true;
                hasMeta |= anno.meta();
                hasModule |= anno.module();
            }

            if (!hasAnno) return;

//            mv.local(BASEMETHOD, "javaMethod", "Lorg/jruby/internal/runtime/methods/HandleMethod;");
//            mv.local(MODULEMETHOD, "moduleMethod", "Lorg/jruby/internal/runtime/methods/HandleMethod;");
//
//            mv.local(RUNTIME, "runtime", RUBY_TYPE);
            mv.aload(RUBYMODULE);
            mv.invokevirtual("org/jruby/RubyModule", "getRuntime", "()Lorg/jruby/Ruby;");
            mv.astore(RUNTIME);

            if (hasMeta || hasModule) {
//                mv.local(SINGLETONCLASS, "singletonClass", RUBYMODULE_TYPE);
                mv.aload(1);
                mv.invokevirtual("org/jruby/RubyModule", "getSingletonClass", "()Lorg/jruby/RubyClass;");
                mv.astore(SINGLETONCLASS);
            }

            Map<CharSequence, List<ExecutableElement>> annotatedMethods = new HashMap<>();
            Map<CharSequence, List<ExecutableElement>> staticAnnotatedMethods = new HashMap<>();

            Set<String> frameAwareMethods = null; // lazy init - there's usually none
            Set<String> scopeAwareMethods = null; // lazy init - there's usually none

            int methodCount = 0;
            for (ExecutableElement method : ElementFilter.methodsIn(cd.getEnclosedElements())) {
                JRubyMethod anno = method.getAnnotation(JRubyMethod.class);
                if (anno == null) continue;

                methodCount++;

                // warn if the method raises any exceptions (JRUBY-4494)
                if (method.getThrownTypes().size() != 0) {
                    System.err.print("Method " + cd.toString() + "." + method.toString() + " should not throw exceptions: ");
                    boolean comma = false;
                    for (TypeMirror thrownType : method.getThrownTypes()) {
                        if (comma) System.err.print(", ");
                        System.err.print(thrownType);
                        comma = true;
                    }
                    System.err.print("\n");
                }

                final String[] names = anno.name();
                CharSequence name = names.length == 0 ? method.getSimpleName() : names[0];

                final Map<CharSequence, List<ExecutableElement>> methodsHash;
                if (method.getModifiers().contains(Modifier.STATIC)) {
                    methodsHash = staticAnnotatedMethods;
                } else {
                    methodsHash = annotatedMethods;
                }

                List<ExecutableElement> methodDescs = methodsHash.get(name);
                if (methodDescs == null) {
                    methodsHash.put(name, methodDescs = new ArrayList<>(4));
                }

                methodDescs.add(method);

                // check for caller frame field reads or writes
                boolean frame = false;
                boolean scope = false;
                for (FrameField field : anno.reads()) {
                    frame |= field.needsFrame();
                    scope |= field.needsScope();
                }
                for (FrameField field : anno.writes()) {
                    frame |= field.needsFrame();
                    scope |= field.needsScope();
                }
                
                if (frame) {
                    if (frameAwareMethods == null) frameAwareMethods = new HashSet<>(4, 1);
                    AnnotationHelper.addMethodNamesToSet(frameAwareMethods, method.getSimpleName().toString(), names, anno.alias());
                }
                if (scope) {
                    if (scopeAwareMethods == null) scopeAwareMethods = new HashSet<>(4, 1);
                    AnnotationHelper.addMethodNamesToSet(scopeAwareMethods, method.getSimpleName().toString(), names, anno.alias());
                }
            }

            if (methodCount == 0) {
                // no annotated methods found, skip
                return;
            }

            classNames.add(getActualQualifiedName(cd));

            processMethodDeclarations(staticAnnotatedMethods);
            for (Map.Entry<CharSequence, List<ExecutableElement>> entry : staticAnnotatedMethods.entrySet()) {
                ExecutableElement decl = entry.getValue().get(0);
                if (!decl.getAnnotation(JRubyMethod.class).omit()) addCoreMethodMapping(entry.getKey(), decl);
            }

            processMethodDeclarations(annotatedMethods);
            for (Map.Entry<CharSequence, List<ExecutableElement>> entry : annotatedMethods.entrySet()) {
                ExecutableElement decl = entry.getValue().get(0);
                if (!decl.getAnnotation(JRubyMethod.class).omit()) addCoreMethodMapping(entry.getKey(), decl);
            }

            mv.voidreturn();
            mv.end();

            // write out a static initializer for frame names, so it only fires once
            mv = new SkinnyMethodAdapter(cw, ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);

            mv.start();

            if (frameAwareMethods != null && ! frameAwareMethods.isEmpty()) {
                mv.ldc(frameAwareMethods.size());
                mv.anewarray("java/lang/String");
                int index = 0;
                for (CharSequence name : frameAwareMethods) {
                    mv.dup();
                    mv.ldc(index++);
                    mv.ldc(name);
                    mv.aastore();
                }
                mv.invokestatic("org/jruby/runtime/MethodIndex", "addFrameAwareMethods", "([Ljava/lang/String;)V");
            }
            if (scopeAwareMethods != null && ! scopeAwareMethods.isEmpty()) {
                mv.ldc(frameAwareMethods.size());
                mv.anewarray("java/lang/String");
                int index = 0;
                for (CharSequence name : scopeAwareMethods) {
                    mv.dup();
                    mv.ldc(index++);
                    mv.ldc(name);
                    mv.aastore();
                }
                mv.invokestatic("org/jruby/runtime/MethodIndex", "addScopeAwareMethods", "([Ljava/lang/String;)V");
            }

            mv.voidreturn();
            mv.end();

            cw.visitEnd();

            new File(SRC_GEN_DIR).mkdirs();
            FileOutputStream fos = new FileOutputStream(SRC_GEN_DIR + qualifiedName + POPULATOR_SUFFIX + ".class");
            fos.write(cw.toByteArray());
            fos.close();
        }
        catch (IOException ex) {
            ex.printStackTrace(System.err);
            System.exit(1);
        }
    }

    public void processMethodDeclarations(Map<CharSequence, List<ExecutableElement>> declarations) {
        for (Map.Entry<CharSequence, List<ExecutableElement>> entry : declarations.entrySet()) {
            List<ExecutableElement> list = entry.getValue();

            if (list.size() == 1) {
                // single method, use normal logic
                processMethodDeclaration(list.get(0));
            } else {
                // multimethod, new logic
                processMethodDeclarationMulti(list);
            }
        }
    }

    public void processMethodDeclaration(ExecutableElement method) {
        processMethodDeclarationMulti(Arrays.asList(method));
    }

    public static long getEncodedSignature(JRubyMethod anno) {
        return encodeSignature(anno.required(), anno.optional(), 0, 0, 0, anno.rest(), false);
    }

    public void processMethodDeclarationMulti(List<ExecutableElement> methods) {
        Handle[] handles = new Handle[5];
        List<ExecutableElementDescriptor> descs = new ArrayList<>();
        boolean meta = false;
        boolean isStatic = false;
        JRubyMethod anno = null;
        int min = Integer.MAX_VALUE;
        int max = 0;

        Map<Handle, ExecutableElementDescriptor> handleToDesc = new HashMap<>();

        for (ExecutableElement method : methods) {
            anno = method.getAnnotation(JRubyMethod.class);
            ExecutableElementDescriptor desc = new ExecutableElementDescriptor(method);
            descs.add(desc);

            if (anno != null && mv != null) {
                isStatic |= desc.isStatic;
                CharSequence qualifiedName = desc.declaringClassName;

                boolean hasContext = desc.hasContext;
                boolean hasBlock = desc.hasBlock;

                StringBuilder buffer = new StringBuilder(method.getReturnType().toString()).append(" foo(");
                boolean first = true;
                for (VariableElement parameter : method.getParameters()) {
                    if (!first) buffer.append(',');
                    first = false;
                    buffer.append(parameter.asType().toString());
                }
                buffer.append(')');

                Handle handle = new Handle(isStatic ? H_INVOKESTATIC : H_INVOKEVIRTUAL, qualifiedName.toString().replace('.', '/'), method.getSimpleName().toString(), Method.getMethod(buffer.toString()).getDescriptor());

                int handleOffset = calculateHandleOffset(method.getParameters().size(), anno.required(), anno.optional(), anno.rest(), isStatic, hasContext, hasBlock);

                handles[handleOffset] = handle;
                handleToDesc.put(handle, desc);

                meta |= anno.meta();

                int specificArity = -1;
                if (desc.optional == 0 && !desc.rest) {
                    if (desc.required == 0) {
                        if (desc.actualRequired <= 3) {
                            specificArity = desc.actualRequired;
                        }
                    } else if (desc.required >= 0 && desc.required <= 3) {
                        specificArity = desc.required;
                    }
                }

                if (specificArity != -1) {
                    if (specificArity < min) min = specificArity;
                    if (specificArity > max) max = specificArity;
                } else {
                    if (desc.required < min) min = desc.required;
                    if (desc.rest) max = Integer.MAX_VALUE;
                    if (desc.required + desc.optional > max) max = desc.required + desc.optional;
                }
            }
        }

        int implClass = meta ? SINGLETONCLASS : CLASS;

        mv.newobj("org/jruby/internal/runtime/methods/HandleMethod");
        mv.dup();

        mv.aload(implClass);
        mv.getstatic(p(Visibility.class), anno.visibility().name(), ci(Visibility.class));
        mv.ldc(encodeSignature(0, 0, 0, 0, 0, true, false));
        mv.ldc(true);
        mv.ldc(anno.notImplemented());

        DescriptorInfo info = new DescriptorInfo(descs);

        mv.ldc(info.getParameterDesc());

        mv.ldc(min);
        mv.ldc(max);

        for (int i = 0; i < 5; i++) {
            if (handles[i] != null) {
                mv.ldc(handles[i]);

                adaptHandle(handleToDesc.get(handles[i]), implClass);
            } else {
                mv.aconst_null();
            }
        }

        Method handleInit = Method.getMethod("void foo(org.jruby.RubyModule, org.jruby.runtime.Visibility, long, boolean, boolean, java.lang.String, int, int, java.util.concurrent.Callable, java.util.concurrent.Callable, java.util.concurrent.Callable, java.util.concurrent.Callable, java.util.concurrent.Callable)");
        mv.invokespecial("org/jruby/internal/runtime/methods/HandleMethod", "<init>", handleInit.getDescriptor());

        mv.astore(BASEMETHOD);

        generateMethodAddCalls(methods.get(0), anno);
    }

    public void adaptHandle(ExecutableElementDescriptor executableElementDescriptor, int implClass) {
        ExecutableElementDescriptor desc = executableElementDescriptor;

        // adapt handle
        mv.aload(RUNTIME);
        mv.ldc(calculateActualRequired(desc.method, desc.method.getParameters().size(), desc.optional, desc.rest, desc.isStatic, desc.hasContext, desc.hasBlock));
        mv.ldc(desc.required);
        mv.ldc(desc.optional);
        mv.ldc(desc.rest);
        mv.ldc(desc.rubyName);
        mv.ldc(Type.getObjectType(desc.declaringClassPath));
        mv.ldc(desc.isStatic);
        mv.ldc(desc.hasContext);
        mv.ldc(desc.hasBlock);
        mv.ldc(desc.anno.frame());
        mv.aload(implClass);
        mv.invokestatic("org/jruby/internal/runtime/methods/InvokeDynamicMethodFactory", "adaptHandle", Method.getMethod("java.util.concurrent.Callable adaptHandle(java.lang.invoke.MethodHandle, org.jruby.Ruby, int, int, int, boolean, java.lang.String, java.lang.Class, boolean, boolean, boolean, boolean, org.jruby.RubyModule)").getDescriptor());
    }

    private void addCoreMethodMapping(CharSequence rubyName, ExecutableElement decl) {
        mv.aload(RUNTIME);
        mv.ldc(((TypeElement) decl.getEnclosingElement()).getQualifiedName().toString());
        mv.ldc(decl.getSimpleName().toString());
        mv.ldc(rubyName.toString());
        mv.invokevirtual("org/jruby/Ruby", "addBoundMethod", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
    }

    private static CharSequence getActualQualifiedName(TypeElement td) {
        if (td.getNestingKind() == NestingKind.MEMBER) {
            return getActualQualifiedName((TypeElement)td.getEnclosingElement()) + "$" + td.getSimpleName();
        }
        return td.getQualifiedName().toString();
    }

    // FIXME: duplicated from Signature, since it pulls in org.jruby.Ruby

    private static final int MAX_ENCODED_ARGS_EXPONENT = 8;
    private static final int MAX_ENCODED_ARGS_MASK = 0xFF;
    private static final int ENCODE_RESTKWARGS_SHIFT = 0;
    private static final int ENCODE_REST_SHIFT = ENCODE_RESTKWARGS_SHIFT + 1;
    private static final int ENCODE_REQKWARGS_SHIFT = ENCODE_REST_SHIFT + MAX_ENCODED_ARGS_EXPONENT;
    private static final int ENCODE_KWARGS_SHIFT = ENCODE_REQKWARGS_SHIFT + MAX_ENCODED_ARGS_EXPONENT;
    private static final int ENCODE_POST_SHIFT = ENCODE_KWARGS_SHIFT + MAX_ENCODED_ARGS_EXPONENT;
    private static final int ENCODE_OPT_SHIFT = ENCODE_POST_SHIFT + MAX_ENCODED_ARGS_EXPONENT;
    private static final int ENCODE_PRE_SHIFT = ENCODE_OPT_SHIFT + MAX_ENCODED_ARGS_EXPONENT;

    public static long encodeSignature(int pre, int opt, int post, int kwargs, int requiredKwargs, boolean rest, boolean restKwargs) {
        return
                ((long)pre << ENCODE_PRE_SHIFT) |
                        ((long)opt << ENCODE_OPT_SHIFT) |
                        ((long)post << ENCODE_POST_SHIFT) |
                        ((long)kwargs << ENCODE_KWARGS_SHIFT) |
                        ((long)requiredKwargs << ENCODE_REQKWARGS_SHIFT) |
                        ((rest ? 1 : 0) << ENCODE_REST_SHIFT) |
                        ((restKwargs?1:0) << ENCODE_RESTKWARGS_SHIFT);
    }

    private static int calculateActualRequired(ExecutableElement md, int paramsLength, int optional, boolean rest, boolean isStatic, boolean hasContext, boolean hasBlock) {
        int actualRequired;
        if (optional == 0 && !rest) {
            int args = paramsLength;
            if (args == 0) {
                actualRequired = 0;
            } else {
                if (isStatic) {
                    args--;
                }
                if (hasContext) {
                    args--;
                }
                if (hasBlock) {
                    args--;                        // TODO: confirm expected args are IRubyObject (or similar)
                }
                actualRequired = args;
            }
        } else {
            // optional args, so we have IRubyObject[]
            // TODO: confirm
            int args = paramsLength;
            if (args == 0) {
                actualRequired = 0;
            } else {
                if (isStatic) {
                    args--;
                }
                if (hasContext) {
                    args--;
                }
                if (hasBlock) {
                    args--;                        // minus one more for IRubyObject[]
                }
                args--;

                // TODO: confirm expected args are IRubyObject (or similar)
                actualRequired = args;
            }

            if (actualRequired != 0) {
                throw new RuntimeException("Combining specific args with IRubyObject[] is not yet supported: "
                        + ((TypeElement)md.getEnclosingElement()).getQualifiedName() + "." + md.toString());
            }
        }

        return actualRequired;
    }

    private static int calculateHandleOffset(int paramsLength, int required, int optional, boolean rest, boolean isStatic, boolean hasContext, boolean hasBlock) {
        if (required < 4 && optional == 0 && !rest) {
            int args = paramsLength;
            if (args == 0) {
                return 0;
            } else {
                if (isStatic) {
                    args--;
                }
                if (hasContext) {
                    args--;
                }
                if (hasBlock) {
                    args--;                        // TODO: confirm expected args are IRubyObject (or similar)
                }
                return args;
            }
        } else {
            return 4;
        }
    }

    public void generateMethodAddCalls(ExecutableElement md, JRubyMethod jrubyMethod) {
        final String[] names = jrubyMethod.name();
        final String[] aliases = jrubyMethod.alias();
        if (jrubyMethod.meta()) {
            defineMethodOnClass(BASEMETHOD, SINGLETONCLASS, names, aliases, md);
        } else {
            defineMethodOnClass(BASEMETHOD, CLASS, names, aliases, md);
            if (jrubyMethod.module()) {
                mv.aload(CLASS);
                mv.aload(BASEMETHOD);
                mv.invokestatic("org/jruby/anno/TypePopulator", "populateModuleMethod", "(Lorg/jruby/RubyModule;Lorg/jruby/internal/runtime/methods/DynamicMethod;)Lorg/jruby/internal/runtime/methods/DynamicMethod;");
                mv.astore(MODULEMETHOD);
                defineMethodOnClass(MODULEMETHOD, SINGLETONCLASS, names, aliases, md);
            }
        }
    }

    private void defineMethodOnClass(int methodVar, int classVar, final String[] names, final String[] aliases, ExecutableElement md) {
        final String baseName;
        if (names.length == 0) {
            baseName = md.getSimpleName().toString();
            mv.aload(classVar);
            mv.ldc(baseName);
            mv.aload(methodVar);
            mv.invokevirtual("org/jruby/RubyModule", "addMethodAtBootTimeOnly", "(Ljava/lang/String;Lorg/jruby/internal/runtime/methods/DynamicMethod;)V");
        } else {
            baseName = names[0];
            for (String name : names) {
                mv.aload(classVar);
                mv.ldc(name);
                mv.aload(methodVar);
                mv.invokevirtual("org/jruby/RubyModule", "addMethodAtBootTimeOnly", "(Ljava/lang/String;Lorg/jruby/internal/runtime/methods/DynamicMethod;)V");
            }
        }

        if (aliases.length > 0) {
            for (String alias : aliases) {
                mv.aload(classVar);
                mv.ldc(alias);
                mv.ldc(baseName);
                mv.invokevirtual("org/jruby/RubyModule", "defineAlias", "(Ljava/lang/String;Ljava/lang/String;)V");
            }
        }
    }
}
