/**
 * 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.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.internal.Errors.newReferenceError;
import static com.github.anba.es6draft.runtime.language.IteratorOperations.EnumerateObjectProperties;
import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;

import java.util.HashSet;
import java.util.Set;

import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.types.BuiltinSymbol;
import com.github.anba.es6draft.runtime.types.PropertyDescriptor;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;

/**
 * <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.2 Object Environment Records
 * </ul>
 */
public final class ObjectEnvironmentRecord implements EnvironmentRecord {
    private final ExecutionContext cx;
    private final ScriptObject bindings;
    private final boolean withEnvironment;

    public ObjectEnvironmentRecord(ExecutionContext cx, ScriptObject bindings, boolean withEnvironment) {
        this.cx = cx;
        this.bindings = bindings;
        this.withEnvironment = withEnvironment;
    }

    @Override
    public String toString() {
        return String.format("%s: {%n\twithEnvironment=%b,%n\tbindings=%n\t%s%n}", getClass().getSimpleName(),
                withEnvironment, bindings);
    }

    @Override
    public Set<String> bindingNames() {
        HashSet<String> names = new HashSet<>();
        EnumerateObjectProperties(cx, bindings).forEachRemaining(names::add);
        return names;
    }

    /**
     * 8.1.1.2.1 HasBinding(N)
     */
    @Override
    public boolean hasBinding(String name) {
        /* step 1 (omitted) */
        /* step 2 */
        ScriptObject bindings = this.bindings;
        /* step 3 */
        boolean foundBinding = HasProperty(cx, bindings, name);
        /* step 4 */
        if (!foundBinding) {
            return false;
        }
        /* step 5 */
        if (!withEnvironment) {
            return true;
        }
        /* step 6 */
        Object unscopables = Get(cx, bindings, BuiltinSymbol.unscopables.get());
        /* step 7 */
        if (Type.isObject(unscopables)) {
            boolean blocked = ToBoolean(Get(cx, Type.objectValue(unscopables), name));
            if (blocked) {
                return false;
            }
        }
        /* step 8 */
        return true;
    }

    /**
     * 8.1.1.2.2 CreateMutableBinding (N,D)
     */
    @Override
    public void createMutableBinding(String name, boolean deletable) {
        /* steps 1-2 (omitted) */
        /* steps 3-4 */
        PropertyDescriptor desc = new PropertyDescriptor(UNDEFINED, true, true, deletable);
        DefinePropertyOrThrow(cx, bindings, name, desc);
    }

    /**
     * 8.1.1.2.3 CreateImmutableBinding (N, S)
     */
    @Override
    public void createImmutableBinding(String name, boolean strict) {
        throw new IllegalStateException();
    }

    /**
     * 8.1.1.2.4 InitializeBinding (N,V)
     */
    @Override
    public void initializeBinding(String name, Object value) {
        assert value != null;
        /* step 1 (omitted) */
        /* steps 2-3 (not applicable) */
        /* step 4 */
        setMutableBinding(name, value, false);
    }

    /**
     * 8.1.1.2.5 SetMutableBinding (N,V,S)
     */
    @Override
    public void setMutableBinding(String name, Object value, boolean strict) {
        /* steps 1-2 (omitted) */
        /* step 3 */
        Set(cx, bindings, name, value, strict);
    }

    /**
     * 8.1.1.2.6 GetBindingValue(N,S)
     */
    @Override
    public Object getBindingValue(String name, boolean strict) {
        /* step 1 (omitted) */
        /* step 2 */
        ScriptObject bindings = this.bindings;
        /* step 3 */
        boolean foundBinding = HasProperty(cx, bindings, name);
        /* step 4 */
        if (!foundBinding) {
            if (!strict) {
                return UNDEFINED;
            }
            throw newReferenceError(cx, Messages.Key.UnresolvableReference, name);
        }
        /* step 5 */
        return Get(cx, bindings, name);
    }

    /**
     * 8.1.1.2.7 DeleteBinding (N)
     */
    @Override
    public boolean deleteBinding(String name) {
        /* steps 1-2 (omitted) */
        /* step 3 */
        return bindings.delete(cx, name);
    }

    /**
     * 8.1.1.2.8 HasThisBinding ()
     */
    @Override
    public boolean hasThisBinding() {
        /* step 1 */
        return false;
    }

    /**
     * -
     */
    @Override
    public Object getThisBinding(ExecutionContext cx) {
        throw new IllegalStateException();
    }

    /**
     * 8.1.1.2.9 HasSuperBinding ()
     */
    @Override
    public boolean hasSuperBinding() {
        /* step 1 */
        return false;
    }

    /**
     * 8.1.1.2.10 WithBaseObject()
     */
    @Override
    public ScriptObject withBaseObject() {
        /* step 1 (omitted) */
        /* step 2 */
        if (withEnvironment) {
            return bindings;
        }
        /* step 3 */
        return null;
    }
}
