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

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

import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.types.Constructor;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.builtins.FunctionObject;

/**
 * <h1>8 Executable Code and Execution Contexts</h1><br>
 * <h2>8.1 Lexical Environments</h2><br>
 * <h3>8.1.1 Environment Records</h3>
 * <ul>
 * <li>8.1.1.3 Function Environment Records
 * </ul>
 */
public final class FunctionEnvironmentRecord extends DeclarativeEnvironmentRecord {
    private final FunctionObject functionObject;
    private final Constructor newTarget;
    private final ScriptObject homeObject;
    private Object thisValue;
    private ThisBindingStatus thisBindingStatus;

    public enum ThisBindingStatus {
        Lexical, Initialized, Uninitialized
    }

    public FunctionEnvironmentRecord(ExecutionContext cx, FunctionObject functionObject, Constructor newTarget,
            Object thisValue) {
        super(cx, false);
        this.functionObject = functionObject;
        this.newTarget = newTarget;
        this.homeObject = functionObject.getHomeObject();
        this.thisValue = thisValue;
        this.thisBindingStatus = thisValue != null ? ThisBindingStatus.Initialized : ThisBindingStatus.Lexical;
    }

    public FunctionEnvironmentRecord(ExecutionContext cx, FunctionObject functionObject, Constructor newTarget) {
        super(cx, false);
        this.functionObject = functionObject;
        this.newTarget = newTarget;
        this.homeObject = functionObject.getHomeObject();
        this.thisValue = UNDEFINED;
        this.thisBindingStatus = ThisBindingStatus.Uninitialized;
    }

    @Override
    public String toString() {
        return String.format(
                "%s:{%n\tfunctionObject=%s,%n\tnewTarget=%s,%n\thomeObject=%s,%n\tthisValue=%s,%n\tthisBindingStatus=%s,%n\tbindings=%s%n}",
                getClass().getSimpleName(), functionObject, newTarget, homeObject, thisValue, thisBindingStatus,
                bindingsToString());
    }

    /**
     * Returns the {@code FunctionObject} field.
     * 
     * @return the {@code FunctionObject} field
     */
    public FunctionObject getFunctionObject() {
        return functionObject;
    }

    /**
     * Returns the {@code newTarget} field.
     * 
     * @return the {@code newTarget} field
     */
    public Constructor getNewTarget() {
        return newTarget;
    }

    /**
     * Returns the {@code homeObject} field.
     * 
     * @return the {@code homeObject} field
     */
    public ScriptObject getHomeObject() {
        return homeObject;
    }

    /**
     * Returns the {@code thisValue} field.
     * 
     * @return the {@code thisValue} field
     */
    public Object getThisValue() {
        return thisValue;
    }

    /**
     * Returns the {@code thisBindingStatus} field.
     * 
     * @return the {@code thisBindingStatus} field
     */
    public ThisBindingStatus getThisBindingStatus() {
        return thisBindingStatus;
    }

    /**
     * 8.1.1.3.1 BindThisValue(V)
     * 
     * @param cx
     *            the execution context
     * @param thisValue
     *            the function {@code this} value
     */
    public void bindThisValue(ExecutionContext cx, ScriptObject thisValue) {
        /* step 1 (not applicable) */
        /* step 2 */
        assert thisBindingStatus != ThisBindingStatus.Lexical;
        /* step 3 */
        if (thisBindingStatus == ThisBindingStatus.Initialized) {
            throw newReferenceError(cx, Messages.Key.InitializedThis);
        }
        /* step 4 */
        this.thisValue = thisValue;
        /* step 5 */
        this.thisBindingStatus = ThisBindingStatus.Initialized;
        /* step 6 (not applicable) */
    }

    /**
     * 8.1.1.3.2 HasThisBinding ()
     */
    @Override
    public boolean hasThisBinding() {
        /* steps 1-2 */
        return thisValue != null;
    }

    /**
     * 8.1.1.3.3 HasSuperBinding ()
     */
    @Override
    public boolean hasSuperBinding() {
        /* steps 1-3 */
        return thisValue != null && homeObject != null;
    }

    /**
     * 8.1.1.3.4 GetThisBinding ()
     * 
     * @param cx
     *            the execution context
     */
    @Override
    public Object getThisBinding(ExecutionContext cx) {
        /* step 1 (not applicable) */
        /* step 2 */
        assert thisBindingStatus != ThisBindingStatus.Lexical;
        /* step 3 */
        if (thisBindingStatus == ThisBindingStatus.Uninitialized) {
            throw newReferenceError(cx, Messages.Key.UninitializedThis);
        }
        /* step 4 */
        return thisValue;
    }

    /**
     * 8.1.1.3.5 GetSuperBase ()
     * 
     * @param cx
     *            the execution context
     * @return the prototype of the home object or {@code null} if no super binding was set
     */
    public ScriptObject getSuperBase(ExecutionContext cx) {
        /* step 1 (not applicable) */
        /* step 2 */
        ScriptObject home = homeObject;
        /* step 3 */
        if (home == null) {
            return null;
        }
        /* step 4 (not applicable) */
        /* step 5 */
        return home.getPrototypeOf(cx);
    }
}
