/*
 * Copyright (c) 2017-2020 Software Architecture Group, Hasso Plattner Institute
 *
 * Licensed under the MIT License.
 */
package de.hpi.swa.trufflesqueak.nodes;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.NodeCost;
import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.profiles.ConditionProfile;

import de.hpi.swa.trufflesqueak.SqueakLanguage;
import de.hpi.swa.trufflesqueak.exceptions.SqueakExceptions.SqueakError;
import de.hpi.swa.trufflesqueak.exceptions.SqueakExceptions.SqueakSyntaxError;
import de.hpi.swa.trufflesqueak.image.SqueakImageContext;
import de.hpi.swa.trufflesqueak.model.ClassObject;
import de.hpi.swa.trufflesqueak.model.CompiledCodeObject;
import de.hpi.swa.trufflesqueak.model.CompiledMethodObject;
import de.hpi.swa.trufflesqueak.model.NativeObject;
import de.hpi.swa.trufflesqueak.model.PointersObject;
import de.hpi.swa.trufflesqueak.nodes.DispatchSendFromStackNodeFactory.DispatchSendSelectorNodeGen;
import de.hpi.swa.trufflesqueak.nodes.accessing.AbstractPointersObjectNodes.AbstractPointersObjectWriteNode;
import de.hpi.swa.trufflesqueak.nodes.accessing.SqueakObjectClassNode;
import de.hpi.swa.trufflesqueak.nodes.context.frame.FrameStackPopNNode;
import de.hpi.swa.trufflesqueak.nodes.context.frame.FrameStackPopNode;
import de.hpi.swa.trufflesqueak.util.MiscUtils;

@NodeInfo(cost = NodeCost.NONE)
public abstract class DispatchSendFromStackNode extends AbstractNode {

    public static DispatchSendFromStackNode create(final NativeObject selector, final CompiledCodeObject code, final int argumentCount) {
        if (code.image.isHeadless()) {
            if (selector.isDebugErrorSelector()) {
                return new DispatchSendHeadlessErrorNode();
            } else if (selector.isDebugSyntaxErrorSelector()) {
                return new DispatchSendSyntaxErrorNode(argumentCount);
            }
        }
        return DispatchSendSelectorNodeGen.create(argumentCount);
    }

    public abstract Object executeSend(VirtualFrame frame, NativeObject selector, Object lookupResult, Object receiver, ClassObject rcvrClass);

    public abstract static class DispatchSendSelectorNode extends DispatchSendFromStackNode {
        protected final int argumentCount;

        protected DispatchSendSelectorNode(final int argumentCount) {
            this.argumentCount = argumentCount;
        }

        @Specialization(guards = {"lookupResult != null"})
        protected static final Object doDispatch(final VirtualFrame frame, @SuppressWarnings("unused") final NativeObject selector, final CompiledMethodObject lookupResult,
                        @SuppressWarnings("unused") final Object receiver, @SuppressWarnings("unused") final ClassObject rcvrClass,
                        @Cached("create(argumentCount)") final DispatchEagerlyFromStackNode dispatchNode) {
            return dispatchNode.executeDispatch(frame, lookupResult);
        }

        @Specialization(guards = {"lookupResult == null"})
        protected static final Object doDoesNotUnderstand(final VirtualFrame frame, final NativeObject selector, @SuppressWarnings("unused") final Object lookupResult, final Object receiver,
                        final ClassObject rcvrClass,
                        @Shared("writeNode") @Cached final AbstractPointersObjectWriteNode writeNode,
                        @Cached final LookupMethodNode lookupNode,
                        @Cached("create(argumentCount)") final FrameStackPopNNode popArguments,
                        @Cached final FrameStackPopNode popReceiver,
                        @Cached("create()") final DispatchEagerlyNode dispatchNode,
                        @CachedContext(SqueakLanguage.class) final SqueakImageContext image) {
            final CompiledMethodObject doesNotUnderstandMethod = (CompiledMethodObject) lookupNode.executeLookup(rcvrClass, image.doesNotUnderstand);
            final PointersObject message = image.newMessage(writeNode, selector, rcvrClass, popArguments.execute(frame));
            final Object poppedReceiver = popReceiver.execute(frame);
            assert receiver == poppedReceiver;
            return dispatchNode.executeDispatch(frame, doesNotUnderstandMethod, new Object[]{receiver, message});
        }

        @Specialization(guards = {"!isCompiledMethodObject(targetObject)"})
        protected static final Object doObjectAsMethod(final VirtualFrame frame, final NativeObject selector, final Object targetObject, final Object receiver,
                        @SuppressWarnings("unused") final ClassObject rcvrClass,
                        @Cached final SqueakObjectClassNode classNode,
                        @Shared("writeNode") @Cached final AbstractPointersObjectWriteNode writeNode,
                        @Cached final LookupMethodNode lookupNode,
                        @Cached("create(argumentCount)") final FrameStackPopNNode popArguments,
                        @Cached final FrameStackPopNode popReceiver,
                        @Cached("createBinaryProfile()") final ConditionProfile isDoesNotUnderstandProfile,
                        @Cached("create()") final DispatchEagerlyNode dispatchNode,
                        @CachedContext(SqueakLanguage.class) final SqueakImageContext image) {
            final Object[] arguments = popArguments.execute(frame);
            final Object poppedReceiver = popReceiver.execute(frame);
            assert receiver == poppedReceiver;
            final ClassObject targetClass = classNode.executeLookup(targetObject);
            final Object newLookupResult = lookupNode.executeLookup(targetClass, image.runWithInSelector);
            if (isDoesNotUnderstandProfile.profile(newLookupResult == null)) {
                final Object doesNotUnderstandMethod = lookupNode.executeLookup(targetClass, image.doesNotUnderstand);
                return dispatchNode.executeDispatch(frame, (CompiledMethodObject) doesNotUnderstandMethod,
                                new Object[]{targetObject, image.newMessage(writeNode, selector, targetClass, arguments)});
            } else {
                return dispatchNode.executeDispatch(frame, (CompiledMethodObject) newLookupResult, new Object[]{targetObject, selector, image.asArrayOfObjects(arguments), receiver});
            }
        }
    }

    private static final class DispatchSendHeadlessErrorNode extends DispatchSendFromStackNode {
        @Override
        public Object executeSend(final VirtualFrame frame, final NativeObject selector, final Object lookupResult, final Object receiver, final ClassObject rcvrClass) {
            CompilerDirectives.transferToInterpreter();
            throw new SqueakError(this, MiscUtils.format("%s>>#%s detected in headless mode. Aborting...", rcvrClass.getSqueakClassName(), selector.asStringUnsafe()));
        }
    }

    private static final class DispatchSendSyntaxErrorNode extends DispatchSendFromStackNode {
        private final int argumentCount;

        private DispatchSendSyntaxErrorNode(final int argumentCount) {
            this.argumentCount = argumentCount;
        }

        @Override
        public Object executeSend(final VirtualFrame frame, final NativeObject selector, final Object lookupResult, final Object receiver, final ClassObject rcvrClass) {
            CompilerDirectives.transferToInterpreter();
            // TODO: make this better
            throw new SqueakSyntaxError((PointersObject) FrameStackPopNNode.create(argumentCount).execute(frame)[0]);
        }
    }
}