/* *****************************************************************************
 * Copyright 2018 Dynamic Analysis Group, Università della Svizzera Italiana (USI)
 * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * *****************************************************************************/
package ch.usi.inf.nodeprof.handlers;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Scope;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.EventContext;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.js.nodes.function.FunctionBodyNode;
import com.oracle.truffle.js.nodes.function.JSBuiltinNode;
import com.oracle.truffle.js.runtime.JSFrameUtil;
import com.oracle.truffle.js.runtime.objects.Undefined;
import com.oracle.truffle.regex.RegexBodyNode;
import com.oracle.truffle.regex.RegexRootNode;

import ch.usi.inf.nodeprof.ProfiledTagEnum;

/**
 * Abstract event handler for function roots
 */
public abstract class FunctionRootEventHandler extends BaseSingleTagEventHandler {
    protected final String funcName = context.getInstrumentedNode().getRootNode().getName();
    protected final boolean isRegExp = context.getInstrumentedNode().getRootNode() instanceof RegexRootNode || context.getInstrumentedNode() instanceof RegexBodyNode;
    protected final boolean isBuiltin = context.getInstrumentedNode() instanceof JSBuiltinNode;

    protected final String builtinName;

    @CompilationFinal private FrameSlot thisSlot;

    @CompilationFinal private boolean thisSlotInitialized = false;

    public FunctionRootEventHandler(EventContext context) {
        super(context, ProfiledTagEnum.ROOT);
        if (isBuiltin) {
            builtinName = getAttribute("name").toString();
        } else {
            builtinName = null;
        }
    }

    @Override
    protected SourceSection getSourceSectionForIID() {
        return context.getInstrumentedNode().getRootNode().getSourceSection();
    }

    public Object getReceiver(VirtualFrame frame, TruffleInstrument.Env env) {
        // cache the frame slot for `this`
        if (!thisSlotInitialized) {
            CompilerDirectives.transferToInterpreterAndInvalidate();
            thisSlot = JSFrameUtil.getThisSlot(frame.getFrameDescriptor());
            thisSlotInitialized = true;
        }
        // if function has a <this> slot and its value is not undefined, we have a shortcut to
        // `this`
        if (thisSlot != null) {
            Object maybeThis = frame.getValue(thisSlot);
            if (maybeThis != null && maybeThis != Undefined.instance) {
                return maybeThis;
            }
        }

        // otherwise, retrieve the current scope to look up this
        return getReceiverFromScope(frame.materialize(), env);
    }

    @TruffleBoundary
    private Object getReceiverFromScope(MaterializedFrame frame, TruffleInstrument.Env env) {
        Iterable<Scope> scopes = env.findLocalScopes(context.getInstrumentedNode(), frame);
        assert scopes.iterator().hasNext();
        Object receiver = scopes.iterator().next().getReceiver();
        assert receiver != null;
        return receiver;
    }

    public boolean isRegularExpression() {
        return this.isRegExp;
    }

    public boolean isBuiltin() {
        return this.isBuiltin;
    }

    public Object getBuiltinName() {
        return this.isBuiltin ? this.builtinName : Undefined.instance;
    }

    public Object getFunction(VirtualFrame frame) {
        return frame.getArguments()[1];
    }

    public Object[] getArguments(VirtualFrame frame) {
        return frame.getArguments();
    }

    public Object getArgument(VirtualFrame frame, int index) {
        return getArguments(frame)[2 + index];
    }

    public String getFunctionName() {
        return this.funcName;
    }

    /**
     * @return the source of the instrumented node (or its closest parent), or null if no source is
     *         available
     */
    public Source getSource() {
        if (isRegularExpression() || this.isBuiltin) {
            return null;
        }

        Node n = context.getInstrumentedNode();
        while (n != null && !(n instanceof FunctionBodyNode)) {
            n = n.getParent();
        }

        if (n == null) {
            return null;
        }

        if (n.getSourceSection() == null) {
            return null;
        }

        return n.getSourceSection().getSource();
    }
}