/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 2.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 2.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-v20.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.internal.runtime;

import java.util.Collection;

import org.jruby.Ruby;
import org.jruby.RubyModule;
import org.jruby.compiler.Compilable;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.internal.runtime.methods.IRMethodArgs;
import org.jruby.internal.runtime.methods.MixedModeIRMethod;
import org.jruby.ir.IRFlags;
import org.jruby.ir.IRMethod;
import org.jruby.ir.IRScope;
import org.jruby.ir.interpreter.InterpreterContext;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ArgumentDescriptor;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.PositionAware;
import org.jruby.runtime.Signature;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.ivars.MethodData;
import org.jruby.util.cli.Options;

import static org.jruby.api.Access.instanceConfig;

public abstract class AbstractIRMethod extends DynamicMethod implements IRMethodArgs, PositionAware, Cloneable {

    protected final Signature signature;
    protected IRScope method;
    protected final int line;
    protected final StaticScope staticScope;
    protected int callCount = 0;
    protected transient InterpreterContext interpreterContext; // cached from method
    private transient MethodData methodData;
    private boolean ruby2Keywords;

    // Interpreted and Jitted but live IRScope known constructor
    public AbstractIRMethod(IRScope method, Visibility visibility, RubyModule implementationClass) {
        this(method.getStaticScope(), method.getId(), method.getLine(), visibility, implementationClass);
        // It is a little hinky to have a callback when we just set method anyways, but debugging in main constructor might need method before we set it.
        this.method = method;
    }

    // Compiled where IRScope must be retrieved at a later date if actually needed
    public AbstractIRMethod(StaticScope scope, String id, int line, Visibility visibility,
                            RubyModule implementationClass) {
        super(implementationClass, visibility, id);
        this.staticScope = scope;
        this.staticScope.determineModule();
        this.signature = staticScope.getSignature();
        this.line = line;

        final Ruby runtime = implementationClass.getRuntime();
        // If we are printing, do the build right at creation time so we can see it
        if (IRRuntimeHelpers.shouldPrintIR(runtime) && IRRuntimeHelpers.shouldPrintScope(scope.getIRScope())) {
            ensureInstrsReady();
        }
    }

    protected static <T extends AbstractIRMethod & Compilable> void tryJit(ThreadContext context, T self, boolean force) {
        if (context.runtime.isBooting() && !Options.JIT_KERNEL.load()) return; // don't JIT during runtime boot

        if (self.callCount < 0) return;
        // we don't synchronize callCount++ it does not matter if count isn't accurate
        if (self.callCount++ >= instanceConfig(context).getJitThreshold()) {
            synchronized (self) { // disable same jit tasks from entering queue twice
                if (self.callCount >= 0) {
                    build(context, self, false);
                }
            }
        }
    }

    protected static <T extends AbstractIRMethod & Compilable> void build(ThreadContext context, T self, boolean force) {
        self.callCount = Integer.MIN_VALUE; // so that callCount++ stays < 0

        context.runtime.getJITCompiler().buildThresholdReached(context, self, force);
    }

    public final void setCallCount(int callCount) {
        synchronized (this) {
            this.callCount = callCount;
        }
    }

    // Overrides method in Compilable but this class does not implement that.
    public boolean isBuildComplete() {
        // Successful build and disabled build both set callCount to -1, indicating no further build is possible.
        return callCount < 0;
    }

    public IRScope getIRScope() {
        try {
            if (method == null) method = staticScope.getIRScope();
            return method;
        } catch (Exception e) {
            return null;
        }
    }

    public StaticScope getStaticScope() {
        return staticScope;
    }

    public ArgumentDescriptor[] getArgumentDescriptors() {
        ensureInstrsReady(); // Make sure method is minimally built before returning this info
        return ((IRMethod) getIRScope()).getArgumentDescriptors();
    }

    public InterpreterContext ensureInstrsReady() {
        InterpreterContext interpreterContext = this.interpreterContext;
        if (interpreterContext == null) {
            IRScope method = getIRScope();

            interpreterContext = this.interpreterContext = method.builtInterpreterContext();

            if (IRRuntimeHelpers.shouldPrintIR(implementationClass.getRuntime()) && IRRuntimeHelpers.shouldPrintScope(method)) {
                printMethodIR();
            }
        }
        return interpreterContext;
    }

    protected abstract void printMethodIR() ;

    public Signature getSignature() {
        return signature;
    }

    @Override
    public DynamicMethod dup() {
        return (DynamicMethod) clone();
    }

    @Override
    public Object clone() {
        try {
            AbstractIRMethod clone = (AbstractIRMethod) super.clone();
            clone.callCount = 0;
            if (clone instanceof MixedModeIRMethod mixedMode) {
                mixedMode.reset();
            }
            return clone;
        } catch (CloneNotSupportedException cnse) {
            throw new RuntimeException("not cloneable: " + this);
        }
    }

    public String getFile() {
        return staticScope.getFile();
    }

    public int getLine() {
        return line;
    }

    /**
     * Additional metadata about this method.
     */
    @Override
    public MethodData getMethodData() {
        if (methodData == null) {
            methodData = ((IRMethod) getIRScope()).getMethodData();
        }

        return methodData;
    }

    @Override
    public Collection<String> getInstanceVariableNames() {
        return staticScope.getInstanceVariableNames();
    }

    @Override
    public String toString() {
        return getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + ' ' + getIRScope() + ' ' + getSignature();
    }

    public boolean needsToFindImplementer() {
        ensureInstrsReady(); // Ensure scope is ready for flags

        IRScope irScope = getIRScope();
        // FIXME: This may stop working if we eliminate startup interp
        return !(irScope instanceof IRMethod && !irScope.getInterpreterContext().getFlags().contains(IRFlags.REQUIRES_CLASS));
    }

    public void setRuby2Keywords() {
        ruby2Keywords = true;
        // FIXME: This is conditionally true and should be passed in or accessed from method and not scope.
        getIRScope().setRuby2Keywords();
    }

    public boolean getRuby2Keywords() {
        return ruby2Keywords;
    }

    /*
     * Calls a split method (java constructor-invoked initialize) and returns the paused state. If
     * this method doesn't have a super call, returns null without execution.
     */
    public abstract SplitSuperState startSplitSuperCall(ThreadContext context, IRubyObject self, RubyModule klazz,
            String name, IRubyObject[] args, Block block);
    
    public abstract void finishSplitCall(SplitSuperState state);
}
