/**
 * Copyright (c) André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.runtime.objects.async;

import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;

import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.internal.CodeContinuation;
import com.github.anba.es6draft.runtime.internal.Continuation;
import com.github.anba.es6draft.runtime.internal.ResumptionPoint;
import com.github.anba.es6draft.runtime.internal.RuntimeInfo;
import com.github.anba.es6draft.runtime.internal.ScriptException;
import com.github.anba.es6draft.runtime.objects.promise.PromiseCapability;
import com.github.anba.es6draft.runtime.objects.promise.PromiseObject;

/**
 * <h1>Async Functions</h1>
 * 
 * {@link Async} implementation for async functions.
 */
public final class AsyncObject implements Async {
    public enum AsyncState {
        SuspendedAwait, Executing, Completed
    }

    private final PromiseCapability<PromiseObject> promiseCapability;
    private RuntimeInfo.Function code;
    private ExecutionContext context;
    private AsyncState state;
    private Continuation<Void> continuation;

    AsyncObject(PromiseCapability<PromiseObject> promiseCapability) {
        this.promiseCapability = promiseCapability;
    }

    /**
     * Returns the current async state.
     *
     * @return the async state
     */
    public AsyncState getState() {
        return state;
    }

    /**
     * Proceeds to the "SuspendedAwait" async state.
     */
    private void suspend() {
        assert state == AsyncState.Executing : "suspend from: " + state;
        state = AsyncState.SuspendedAwait;
    }

    /**
     * Proceeds to the "completed" async state and releases internal resources.
     */
    private void close() {
        assert state == AsyncState.Executing : "close from: " + state;
        state = AsyncState.Completed;
        context = null;
        code = null;
        continuation = null;
    }

    /**
     * Starts async function execution.
     * 
     * @param cx
     *            the execution context
     * @param functionCode
     *            the runtime function code
     */
    void start(ExecutionContext cx, RuntimeInfo.Function functionCode) {
        assert state == null;
        context = cx;
        code = functionCode;
        state = AsyncState.Executing;
        context.setCurrentAsync(this);
        continuation = new CodeContinuation<>(new AsyncHandler(this));
        continuation.start(cx);
    }

    @Override
    public void resume(ExecutionContext cx, Object value) {
        switch (state) {
        case Executing:
        case Completed:
            throw new AssertionError();
        case SuspendedAwait:
            state = AsyncState.Executing;
            continuation.resume(cx, value);
            return;
        default:
            throw new AssertionError();
        }
    }

    @Override
    public void _throw(ExecutionContext cx, Object value) {
        switch (state) {
        case Executing:
        case Completed:
            throw new AssertionError();
        case SuspendedAwait:
            state = AsyncState.Executing;
            continuation._throw(cx, ScriptException.create(value));
            return;
        default:
            throw new AssertionError();
        }
    }

    private static final class AsyncHandler implements Continuation.Handler<Void> {
        private final AsyncObject asyncObject;

        AsyncHandler(AsyncObject asyncObject) {
            this.asyncObject = asyncObject;
        }

        @Override
        public Object evaluate(ResumptionPoint resumptionPoint) throws Throwable {
            AsyncObject asyncObj = asyncObject;
            return asyncObj.code.handle().invokeExact(asyncObj.context, resumptionPoint);
        }

        @Override
        public void close() {
            asyncObject.close();
        }

        @Override
        public Void suspendWith(ExecutionContext cx, Object value) {
            asyncObject.suspend();
            return null;
        }

        @Override
        public Void returnWith(ExecutionContext cx, Object result) {
            // Async execution finished, run AsyncFunctionStart steps 4-9.
            asyncObject.promiseCapability.getResolve().call(cx, UNDEFINED, result);
            return null;
        }

        @Override
        public Void returnWith(ExecutionContext cx, ScriptException exception) {
            // Async execution finished, run AsyncFunctionStart steps 4-9.
            asyncObject.promiseCapability.getReject().call(cx, UNDEFINED, exception.getValue());
            return null;
        }
    }
}
