/*
 ***** 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.
 *
 * 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.specialized;

import me.qmx.jitescript.CodeBlock;
import me.qmx.jitescript.JDKVersion;
import me.qmx.jitescript.JiteClass;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyObject;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ClassDefiningClassLoader;
import org.jruby.util.OneShotClassLoader;
import org.jruby.util.cli.Options;
import org.jruby.util.collections.NonBlockingHashMapLong;
import org.objectweb.asm.Label;
import org.objectweb.asm.tree.LabelNode;

import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Set;

import static org.jruby.util.CodegenUtils.ci;
import static org.jruby.util.CodegenUtils.p;
import static org.jruby.util.CodegenUtils.sig;

/**
 * A code generator for Ruby objects, to map known instance variables into fields.
 */
public class RubyObjectSpecializer {

    public static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

    private static ClassAndAllocator getClassForSize(int size) {
        return SPECIALIZED_CLASSES.get(size);
    }

    private static final NonBlockingHashMapLong<ClassAndAllocator> SPECIALIZED_CLASSES = new NonBlockingHashMapLong<>();

    private static final ClassDefiningClassLoader LOADER = new OneShotClassLoader(Ruby.getClassLoader());

    static class ClassAndAllocator {
        final Class cls;
        final ObjectAllocator allocator;

        ClassAndAllocator(Class cls, ObjectAllocator allocator) {
            this.cls = cls;
            this.allocator = allocator;
        }
    }

    public static ObjectAllocator specializeForVariables(RubyClass klass, Set<String> foundVariables) {
        int size = foundVariables.size();

        // clamp to max object width (jruby/jruby#
        size = Math.min(size, Options.REIFY_VARIABLES_MAX.load());

        ClassAndAllocator cna = null;
        String className = null;

        if (Options.REIFY_VARIABLES_NAME.load()) {
            className = klass.getName();

            if (className.startsWith("#")) {
                className = "Anonymous" + Integer.toHexString(System.identityHashCode(klass));
            } else {
                className = className.replace("::", "/");
            }
        } else {
            // Generate class for specified size
            cna = getClassForSize(size);

            if (cna == null) {
                className = "RubyObject" + size;
            }
        }

        // if we have a className, proceed to generate
        if (className != null) {
            final String clsPath = "org/jruby/gen/" + className;

            synchronized (LOADER) {
                Class specialized;
                try {
                    // try loading class without generating
                    specialized = LOADER.loadClass(clsPath.replace('/', '.'));
                } catch (ClassNotFoundException cnfe) {
                    // generate specialized class
                    specialized = generateInternal(size, clsPath);
                }

                try {
                    ObjectAllocator allocator = (ObjectAllocator) specialized.getDeclaredClasses()[0].getConstructor().newInstance();

                    cna = new ClassAndAllocator(specialized, allocator);

                    if (!Options.REIFY_VARIABLES_NAME.load()) {
                        SPECIALIZED_CLASSES.put(size, cna);
                    }
                } catch (Throwable t) {
                    throw new RuntimeException(t);
                }
            }
        }

        // Pre-initialize variable table with field accessors for size
        try {
            int offset = 0;

            // TODO: this just ends up reifying the first N variables it finds, which may not be the most valuable
            for (String name : foundVariables) {
                klass.getVariableTableManager().getVariableAccessorForRubyVar(
                        name,
                        LOOKUP.findGetter(cna.cls, "var" + offset, Object.class),
                        LOOKUP.findSetter(cna.cls, "var" + offset, Object.class));
                offset++;
                if (offset >= size) break;
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        klass.setReifiedClass(cna.cls);
        klass.setAllocator(cna.allocator);

        return cna.allocator;
    }

    private static Class generateInternal(int size, final String clsPath) {
        // ensure only one thread will attempt to generate and define the new class
        final String baseName = p(RubyObject.class);

        final JiteClass jiteClass = new JiteClass(clsPath, baseName, new String[0]) {{
            for (int i = 0; i < size; i++) {
                final int offset = i;

                // fields
                defineField("var" + offset, ACC_PUBLIC, ci(Object.class), null);
            }

            // parent class constructor
            defineMethod("<init>", ACC_PUBLIC, sig(void.class, Ruby.class, RubyClass.class), new CodeBlock() {{
                aload(0);
                aload(1);
                aload(2);
                invokespecial(baseName, "<init>", sig(void.class, Ruby.class, RubyClass.class));
                voidreturn();
            }});

            // required overrides
            defineMethod("getVariable", ACC_PUBLIC, sig(Object.class, int.class), new CodeBlock() {{
                LabelNode parentCall = new LabelNode(new Label());

                line(0);

                if (size > 0) genGetSwitch(clsPath, size, this, 1);

                line(1);

                aload(0);
                iload(1);
                invokespecial(p(RubyObject.class), "getVariable", sig(Object.class, int.class));
                areturn();
            }});

            defineMethod("setVariable", ACC_PUBLIC, sig(void.class, int.class, Object.class), new CodeBlock() {{
                LabelNode parentCall = new LabelNode(new Label());

                line(2);

                if (size > 0) genPutSwitch(clsPath, size, this, 1);

                line(3);

                aload(0);
                iload(1);
                aload(2);
                invokespecial(p(RubyObject.class), "setVariable", sig(void.class, int.class, Object.class));
                voidreturn();
            }});

            // allocator class
            addChildClass(new JiteClass(clsPath + "Allocator", p(Object.class), Helpers.arrayOf(p(ObjectAllocator.class))) {{
                defineDefaultConstructor();

                defineMethod("allocate", ACC_PUBLIC, sig(IRubyObject.class, Ruby.class, RubyClass.class), new CodeBlock() {{
                    newobj(clsPath);
                    dup();
                    aload(1);
                    aload(2);
                    invokespecial(clsPath, "<init>", sig(void.class, Ruby.class, RubyClass.class));
                    areturn();
                }});
            }});
        }};

        Class specializedClass = defineClass(jiteClass);
        defineClass(jiteClass.getChildClasses().get(0));

        return specializedClass;
    }

    private static void genGetSwitch(String clsPath, int size, CodeBlock block, int offsetVar) {
        LabelNode defaultError = new LabelNode(new Label());
        LabelNode[] cases = new LabelNode[size];
        for (int i = 0; i < size; i++) {
            cases[i] = new LabelNode(new Label());
        }
        block.iload(offsetVar);
        block.tableswitch(0, size - 1, defaultError, cases);
        for (int i = 0; i < size; i++) {
            block.label(cases[i]);
            block.aload(0);
            block.getfield(clsPath, "var" + i, ci(Object.class));
            block.areturn();
        }
        block.label(defaultError);
    }

    private static void genPutSwitch(String clsPath, int size, CodeBlock block, int offsetVar) {
        LabelNode defaultError = new LabelNode(new Label());
        LabelNode[] cases = new LabelNode[size];
        for (int i = 0; i < size; i++) {
            cases[i] = new LabelNode(new Label());
        }
        block.iload(offsetVar);
        block.tableswitch(0, size - 1, defaultError, cases);
        for (int i = 0; i < size; i++) {
            block.label(cases[i]);
            block.aload(0);
            block.aload(2);
            block.putfield(clsPath, "var" + i, ci(Object.class));
            block.voidreturn();
        }
        block.label(defaultError);
    }

    private static Class defineClass(JiteClass jiteClass) {
        return LOADER.defineClass(classNameFromJiteClass(jiteClass), jiteClass.toBytes(JDKVersion.V1_8));
    }

    private static String classNameFromJiteClass(JiteClass jiteClass) {
        return jiteClass.getClassName().replace('/', '.');
    }
}
