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

import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

import com.github.anba.es6draft.ast.*;
import com.github.anba.es6draft.ast.Module;
import com.github.anba.es6draft.ast.scope.BlockScope;
import com.github.anba.es6draft.ast.scope.Name;
import com.github.anba.es6draft.ast.synthetic.MethodDefinitionsMethod;
import com.github.anba.es6draft.ast.synthetic.PropertyDefinitionsMethod;
import com.github.anba.es6draft.runtime.internal.InlineArrayList;
import com.github.anba.es6draft.runtime.modules.ExportEntry;
import com.github.anba.es6draft.runtime.modules.ImportEntry;

/**
 * <h1>Static Semantics</h1>
 * <ul>
 * <li>BoundNames
 * <li>ConstructorMethod
 * <li>ContainsExpression
 * <li>ExpectedArgumentCount
 * <li>ExportEntries
 * <li>HasInitializer
 * <li>HasName
 * <li>ImportedLocalNames
 * <li>ImportEntries
 * <li>IsAnonymousFunctionDefinition
 * <li>IsConstantDeclaration
 * <li>IsFunctionDefinition
 * <li>IsIdentifierRef
 * <li>IsSimpleParameterList
 * <li>IsStrict
 * <li>LexicallyDeclaredNames
 * <li>LexicallyScopedDeclarations
 * <li>ModuleRequests
 * <li>PropName
 * <li>SpecialMethod
 * <li>VarDeclaredNames
 * <li>VarScopedDeclarations
 * </ul>
 */
public final class StaticSemantics {
    private StaticSemantics() {
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>12.1.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the binding identifier
     * @return the bound name
     */
    public static Name BoundName(BindingIdentifier node) {
        return node.getName();
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>14.1.3 Static Semantics: BoundNames
     * <li>14.4.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the hoistable declaration
     * @return the bound name
     */
    public static Name BoundName(HoistableDeclaration node) {
        return node.getName();
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>14.5.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the hoistable declaration
     * @return the bound name
     */
    public static Name BoundName(ClassDeclaration node) {
        return node.getName();
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>12.1.2 Static Semantics: BoundNames
     * <li>13.3.3.1 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the binding node
     * @return the bound names
     */
    public static List<Name> BoundNames(BindingIdentifier node) {
        return singletonList(node.getName());
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>12.1.2 Static Semantics: BoundNames
     * <li>13.3.3.1 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the binding node
     * @return the bound names
     */
    public static List<Name> BoundNames(Binding node) {
        if (node instanceof BindingIdentifier) {
            return singletonList(((BindingIdentifier) node).getName());
        }
        assert node instanceof BindingPattern;
        return node.accept(BoundNames.INSTANCE, new InlineArrayList<>());
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>14.1.3 Static Semantics: BoundNames
     * <li>14.2.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the formal parameters list
     * @return the bound names
     */
    public static List<Name> BoundNames(FormalParameterList node) {
        return BoundNames(node.getFormals());
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>14.1.3 Static Semantics: BoundNames
     * <li>14.2.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param formals
     *            the formal parameters list
     * @return the bound names
     */
    public static List<Name> BoundNames(List<FormalParameter> formals) {
        if (formals.isEmpty()) {
            return emptyList();
        }
        if (formals.size() == 1) {
            return BoundNames(formals.get(0));
        }
        InlineArrayList<Name> list = new InlineArrayList<>();
        for (FormalParameter formalParameter : formals) {
            formalParameter.accept(BoundNames.INSTANCE, list);
        }
        return list;
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>14.1.3 Static Semantics: BoundNames
     * <li>14.2.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the formal parameter node
     * @return the bound names
     */
    public static List<Name> BoundNames(FormalParameter node) {
        BindingElementItem element = node.getElement();
        if (element instanceof BindingElement) {
            return BoundNames(((BindingElement) element).getBinding());
        }
        return BoundNames(((BindingRestElement) element).getBinding());
    }

    /**
     * Static Semantics: BoundNames
     * <ul>
     * <li>13.3.1.2 Static Semantics: BoundNames
     * <li>14.1.3 Static Semantics: BoundNames
     * <li>14.4.2 Static Semantics: BoundNames
     * <li>14.5.2 Static Semantics: BoundNames
     * </ul>
     * 
     * @param node
     *            the declaration node
     * @return the bound names
     */
    public static List<Name> BoundNames(Declaration node) {
        if (node instanceof HoistableDeclaration) {
            return singletonList(BoundName((HoistableDeclaration) node));
        }
        if (node instanceof ClassDeclaration) {
            return singletonList(BoundName((ClassDeclaration) node));
        }
        if (node instanceof ExportDefaultExpression) {
            return BoundNames(((ExportDefaultExpression) node).getBinding());
        }
        return BoundNames((LexicalDeclaration) node);
    }

    /**
     * 13.3.2.1 Static Semantics: BoundNames
     * 
     * @param node
     *            the lexical declaration
     * @return the bound names
     */
    public static List<Name> BoundNames(LexicalDeclaration node) {
        if (node.getElements().size() == 1) {
            return BoundNames(node.getElements().get(0).getBinding());
        }
        return node.accept(BoundNames.INSTANCE, new InlineArrayList<>());
    }

    /**
     * 13.3.2.1 Static Semantics: BoundNames
     * 
     * @param node
     *            the variable statement
     * @return the bound names
     */
    public static List<Name> BoundNames(VariableStatement node) {
        if (node.getElements().size() == 1) {
            return BoundNames(node.getElements().get(0));
        }
        return node.accept(BoundNames.INSTANCE, new InlineArrayList<>());
    }

    /**
     * 13.3.2.1 Static Semantics: BoundNames
     * 
     * @param node
     *            the variable statement
     * @return the bound names
     */
    public static List<Name> BoundNames(VariableDeclaration node) {
        return BoundNames(node.getBinding());
    }

    /**
     * 15.2.2.2 Static Semantics: BoundNames
     * 
     * @param node
     *            the import declaration
     * @return the bound names
     */
    public static List<Name> BoundNames(ImportDeclaration node) {
        return node.accept(BoundNames.INSTANCE, new InlineArrayList<>());
    }

    /**
     * 15.2.3.2 Static Semantics: BoundNames
     * 
     * @param node
     *            the export declaration
     * @return the bound names
     */
    public static List<Name> BoundNames(ExportDeclaration node) {
        return node.accept(BoundNames.INSTANCE, new InlineArrayList<>());
    }

    /**
     * 14.5.3 Static Semantics: ConstructorMethod
     * 
     * @param node
     *            the class definition node
     * @return the constructor method if found, otherwise {@code null}
     */
    public static MethodDefinition ConstructorMethod(ClassDefinition node) {
        return node.getConstructor();
    }

    /**
     * 14.5.3 Static Semantics: ConstructorMethod
     * 
     * @param methods
     *            the methods list
     * @return the constructor method if found, otherwise {@code null}
     */
    public static MethodDefinition ConstructorMethod(List<MethodDefinition> methods) {
        for (MethodDefinition m : methods) {
            if (m.isClassConstructor()) {
                return m;
            }
        }
        return null;
    }

    /**
     * Static Semantics: CallConstructorMethod
     * 
     * @param methods
     *            the methods list
     * @return the call constructor method if found, otherwise {@code null}
     */
    public static MethodDefinition CallConstructorMethod(List<MethodDefinition> methods) {
        for (MethodDefinition m : methods) {
            if (m.isCallConstructor()) {
                return m;
            }
        }
        return null;
    }

    /**
     * Static Semantics: ContainsExpression
     * <ul>
     * <li>13.3.3.2 Static Semantics: ContainsExpression
     * <li>14.1.5 Static Semantics: ContainsExpression
     * <li>14.2.4 Static Semantics: ContainsExpression
     * </ul>
     * 
     * @param formals
     *            the formal parameters
     * @return {@code true} if an expression was found
     */
    public static boolean ContainsExpression(FormalParameterList formals) {
        return formals.containsExpression();
    }

    /**
     * Static Semantics: ContainsExpression
     * <ul>
     * <li>13.3.3.2 Static Semantics: ContainsExpression
     * <li>14.1.5 Static Semantics: ContainsExpression
     * <li>14.2.4 Static Semantics: ContainsExpression
     * </ul>
     * 
     * @param formals
     *            the formal parameters
     * @return {@code true} if an expression was found
     */
    public static boolean ContainsExpression(List<FormalParameter> formals) {
        for (FormalParameter formal : formals) {
            BindingElementItem element = formal.getElement();
            if (element instanceof BindingElement && ContainsExpression((BindingElement) element)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 13.3.3.2 Static Semantics: ContainsExpression
     * 
     * @param element
     *            the binding element node
     * @return {@code true} if an expression was found
     */
    private static boolean ContainsExpression(BindingElement element) {
        if (element.getInitializer() != null) {
            return true;
        }
        return ContainsExpression(element.getBinding());
    }

    /**
     * 13.3.3.2 Static Semantics: ContainsExpression
     * 
     * @param binding
     *            the binding node
     * @return {@code true} if an expression was found
     */
    private static boolean ContainsExpression(Binding binding) {
        if (binding instanceof ArrayBindingPattern) {
            return ContainsExpression((ArrayBindingPattern) binding);
        }
        if (binding instanceof ObjectBindingPattern) {
            return ContainsExpression((ObjectBindingPattern) binding);
        }
        assert binding instanceof BindingIdentifier;
        return false;
    }

    /**
     * 13.3.3.2 Static Semantics: ContainsExpression
     * 
     * @param pattern
     *            the array binding pattern node
     * @return {@code true} if an expression was found
     */
    private static boolean ContainsExpression(ArrayBindingPattern pattern) {
        for (BindingElementItem item : pattern.getElements()) {
            if (item instanceof BindingElement && ContainsExpression((BindingElement) item)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 13.3.3.2 Static Semantics: ContainsExpression
     * 
     * @param pattern
     *            the object binding pattern node
     * @return {@code true} if an expression was found
     */
    private static boolean ContainsExpression(ObjectBindingPattern pattern) {
        for (BindingProperty property : pattern.getProperties()) {
            if (property.getPropertyName() instanceof ComputedPropertyName) {
                return true;
            }
            if (property.getInitializer() != null) {
                return true;
            }
            if (ContainsExpression(property.getBinding())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Static Semantics: ExpectedArgumentCount
     * <ul>
     * <li>14.1.6 Static Semantics: ExpectedArgumentCount
     * <li>14.2.5 Static Semantics: ExpectedArgumentCount
     * <li>14.3.3 Static Semantics: ExpectedArgumentCount
     * </ul>
     * 
     * @param formals
     *            the formal parameters list
     * @return the expected arguments count
     */
    public static int ExpectedArgumentCount(FormalParameterList formals) {
        int count = 0;
        for (FormalParameter formal : formals) {
            BindingElementItem element = formal.getElement();
            if (element instanceof BindingRestElement) {
                break;
            }
            if (HasInitializer((BindingElement) element)) {
                break;
            }
            count += 1;
        }
        return count;
    }

    /**
     * 13.3.3.3 Static Semantics: HasInitializer
     * 
     * @param node
     *            the binding element
     * @return {@code true} if the binding element has an initializer
     */
    private static boolean HasInitializer(BindingElement node) {
        return node.getInitializer() != null;
    }

    /**
     * 14.1.9 Static Semantics: IsAnonymousFunctionDefinition (production)
     * 
     * @param node
     *            the expression node
     * @return {@code true} if the node is an anonymous function definition
     */
    public static boolean IsAnonymousFunctionDefinition(Expression node) {
        if (node instanceof FunctionNode) {
            return ((FunctionNode) node).getIdentifier() == null;
        }
        if (node instanceof ClassExpression) {
            return ((ClassExpression) node).getIdentifier() == null;
        }
        return false;
    }

    /**
     * Static Semantics: HasName
     * <ul>
     * <li>14.1.8 Static Semantics: HasName
     * <li>14.2.7 Static Semantics: HasName
     * <li>14.4.7 Static Semantics: HasName
     * </ul>
     * 
     * @param <FUNCTION>
     *            the function type
     * @param node
     *            the function node
     * @return {@code true} if the function has a binding name
     */
    public static <FUNCTION extends Expression & FunctionNode> boolean HasName(FUNCTION node) {
        return node.getIdentifier() != null;
    }

    /**
     * 14.5.6 Static Semantics: HasName
     * 
     * @param node
     *            the function node
     * @return {@code true} if the function has a binding name
     */
    public static boolean HasName(ClassExpression node) {
        return node.getIdentifier() != null;
    }

    /**
     * Static Semantics: IsConstantDeclaration
     * <ul>
     * <li>13.3.1.3 Static Semantics: IsConstantDeclaration
     * <li>14.1.10 Static Semantics: IsConstantDeclaration
     * <li>14.4.8 Static Semantics: IsConstantDeclaration
     * <li>14.5.7 Static Semantics: IsConstantDeclaration
     * <li>15.2.3.7 Static Semantics: IsConstantDeclaration
     * </ul>
     * 
     * @param node
     *            the declaration node
     * @return {@code true} if the node is a constant declaration
     */
    public static boolean IsConstantDeclaration(Declaration node) {
        return node.isConstDeclaration();
    }

    /**
     * Static Semantics: IsFunctionDefinition
     * <ul>
     * <li>12.2.1.3 Static Semantics: IsFunctionDefinition
     * <li>12.2.10.2 Static Semantics: IsFunctionDefinition
     * <li>12.3.1.2 Static Semantics: IsFunctionDefinition
     * <li>12.4.2 Static Semantics: IsFunctionDefinition
     * <li>12.5.2 Static Semantics: IsFunctionDefinition
     * <li>12.6.1 Static Semantics: IsFunctionDefinition
     * <li>12.7.1 Static Semantics: IsFunctionDefinition
     * <li>12.8.1 Static Semantics: IsFunctionDefinition
     * <li>12.9.1 Static Semantics: IsFunctionDefinition
     * <li>12.10.1 Static Semantics: IsFunctionDefinition
     * <li>12.11.1 Static Semantics: IsFunctionDefinition
     * <li>12.12.1 Static Semantics: IsFunctionDefinition
     * <li>12.13.1 Static Semantics: IsFunctionDefinition
     * <li>12.14.2 Static Semantics: IsFunctionDefinition
     * <li>12.15.1 Static Semantics: IsFunctionDefinition
     * <li>14.1.11 Static Semantics: IsFunctionDefinition
     * <li>14.4.9 Static Semantics: IsFunctionDefinition
     * <li>14.5.8 Static Semantics: IsFunctionDefinition
     * </ul>
     * 
     * @param node
     *            the expression node
     * @return {@code true} if the node is a function definition
     */
    public static boolean IsFunctionDefinition(Expression node) {
        return node instanceof FunctionNode || node instanceof ClassExpression;
    }

    /**
     * Static Semantics: IsIdentifierRef
     * <ul>
     * <li>12.2.1.4 Static Semantics: IsIdentifierRef
     * <li>12.3.1.4 Static Semantics: IsIdentifierRef
     * </ul>
     * 
     * @param node
     *            the left-hand side expression node
     * @return {@code true} if node is an identifier
     */
    public static boolean IsIdentifierRef(LeftHandSideExpression node) {
        return node instanceof IdentifierReference && !node.isParenthesized();
    }

    /**
     * Static Semantics: IsSimpleParameterList
     * <ul>
     * <li>14.1.12 Static Semantics: IsSimpleParameterList
     * <li>14.2.8 Static Semantics: IsSimpleParameterList
     * </ul>
     * 
     * @param formals
     *            the formal parameters list
     * @return {@code true} if the list is a simple formal parameters list
     */
    public static boolean IsSimpleParameterList(FormalParameterList formals) {
        return formals.isSimpleParameterList();
    }

    /**
     * Static Semantics: IsSimpleParameterList
     * <ul>
     * <li>14.1.12 Static Semantics: IsSimpleParameterList
     * <li>14.2.8 Static Semantics: IsSimpleParameterList
     * </ul>
     * 
     * @param formals
     *            the formal parameters list
     * @return {@code true} if the list is a simple formal parameters list
     */
    public static boolean IsSimpleParameterList(List<FormalParameter> formals) {
        for (FormalParameter formal : formals) {
            BindingElementItem element = formal.getElement();
            if (element instanceof BindingRestElement) {
                return false;
            }
            if (HasInitializer((BindingElement) element)) {
                return false;
            }
            if (((BindingElement) element).getBinding() instanceof BindingPattern) {
                return false;
            }
        }
        return true;
    }

    /**
     * 15.1.2 Static Semantics: IsStrict
     * 
     * @param node
     *            the script node
     * @return {@code true} if the node is strict mode script
     */
    public static boolean IsStrict(Script node) {
        return node.isStrict();
    }

    /**
     * 15.2.1.9 Static Semantics: IsStrict
     * 
     * @param node
     *            the module node
     * @return {@code true} if the node is strict mode module
     */
    public static boolean IsStrict(Module node) {
        return true;
    }

    /**
     * 14.1.13 Static Semantics: IsStrict
     * 
     * @param node
     *            the function node
     * @return {@code true} if the node is strict mode function
     */
    public static boolean IsStrict(FunctionNode node) {
        return node.getStrictMode() != FunctionNode.StrictMode.NonStrict;
    }

    /**
     * 13.1.6 Static Semantics: LexicallyScopedDeclarations
     * 
     * @param node
     *            the block statement node
     * @return the list of lexically scoped declarations
     */
    public static List<Declaration> LexicallyScopedDeclarations(BlockStatement node) {
        return node.getScope().lexicallyScopedDeclarations();
    }

    /**
     * 13.11.6 Static Semantics: LexicallyScopedDeclarations
     * 
     * @param node
     *            the switch statement node
     * @return the list of lexically scoped declarations
     */
    public static List<Declaration> LexicallyScopedDeclarations(SwitchStatement node) {
        return node.getScope().lexicallyScopedDeclarations();
    }

    /**
     * Static Semantics: LexicallyScopedDeclarations
     * <ul>
     * <li>14.1.15 Static Semantics: LexicallyScopedDeclarations
     * <li>14.2.11 Static Semantics: LexicallyScopedDeclarations
     * </ul>
     * 
     * @param node
     *            the function node
     * @return the list of lexically scoped declarations
     */
    public static List<Declaration> LexicallyScopedDeclarations(FunctionNode node) {
        return node.getScope().lexicallyScopedDeclarations();
    }

    /**
     * 15.1.4 Static Semantics: LexicallyScopedDeclarations
     * 
     * @param node
     *            the script node
     * @return the list of lexically scoped declarations
     */
    public static List<Declaration> LexicallyScopedDeclarations(Script node) {
        return node.getScope().lexicallyScopedDeclarations();
    }

    /**
     * 13.1.5 Static Semantics: LexicallyDeclaredNames
     * 
     * @param node
     *            the block statement node
     * @return the set of lexically declared names
     */
    public static Set<Name> LexicallyDeclaredNames(BlockStatement node) {
        return node.getScope().lexicallyDeclaredNames();
    }

    /**
     * 13.11.5 Static Semantics: LexicallyDeclaredNames
     * 
     * @param node
     *            the switch statement node
     * @return the list of lexically scoped declarations
     */
    public static Set<Name> LexicallyDeclaredNames(SwitchStatement node) {
        return node.getScope().lexicallyDeclaredNames();
    }

    /**
     * Static Semantics: LexicallyDeclaredNames
     * 
     * @param scope
     *            the block scope
     * @return the set of lexically declared names
     */
    public static Set<Name> LexicallyDeclaredNames(BlockScope scope) {
        return scope.lexicallyDeclaredNames();
    }

    /**
     * Static Semantics: LexicallyDeclaredNames
     * <ul>
     * <li>14.1.14 Static Semantics: LexicallyDeclaredNames
     * <li>14.2.10 Static Semantics: LexicallyDeclaredNames
     * </ul>
     * 
     * @param node
     *            the script node
     * @return the set of lexically declared names
     */
    public static Set<Name> LexicallyDeclaredNames(FunctionNode node) {
        return node.getScope().lexicallyDeclaredNames();
    }

    /**
     * 15.1.3 Static Semantics: LexicallyDeclaredNames
     * 
     * @param node
     *            the script node
     * @return the set of lexically declared names
     */
    public static Set<Name> LexicallyDeclaredNames(Script node) {
        return node.getScope().lexicallyDeclaredNames();
    }

    /**
     * Static Semantics: VarDeclaredNames
     * <ul>
     * <li>14.1.16 Static Semantics: VarDeclaredNames
     * <li>14.2.12 Static Semantics: VarDeclaredNames
     * </ul>
     * 
     * <pre>
     * FunctionStatementList :
     *     StatementList
     * </pre>
     * 
     * @param node
     *            the function node
     * @return the set of variable declared names
     */
    public static Set<Name> VarDeclaredNames(FunctionNode node) {
        return node.getScope().varDeclaredNames();
    }

    /**
     * 15.1.5 Static Semantics: VarDeclaredNames
     * 
     * @param node
     *            the script node
     * @return the set of variable declared names
     */
    public static Set<Name> VarDeclaredNames(Script node) {
        return node.getScope().varDeclaredNames();
    }

    /**
     * Static Semantics: VarScopedDeclarations
     * <ul>
     * <li>14.1.17 Static Semantics: VarScopedDeclarations
     * <li>14.2.13 Static Semantics: VarScopedDeclarations
     * </ul>
     * 
     * @param node
     *            the function node
     * @return the list of variable scoped declarations
     */
    public static List<StatementListItem> VarScopedDeclarations(FunctionNode node) {
        return node.getScope().varScopedDeclarations();
    }

    /**
     * 15.1.6 Static Semantics: VarScopedDeclarations
     * 
     * @param node
     *            the script node
     * @return the list of variable scoped declarations
     */
    public static List<StatementListItem> VarScopedDeclarations(Script node) {
        return node.getScope().varScopedDeclarations();
    }

    /**
     * 15.2.1.7 Static Semantics: ExportEntries<br>
     * 15.2.3.5 Static Semantics: ExportEntries
     * 
     * @param node
     *            the module node
     * @return the list of export entries
     */
    public static List<ExportEntry> ExportEntries(Module node) {
        ArrayList<ExportEntry> entries = new ArrayList<>();
        for (ModuleItem item : node.getStatements()) {
            if (item instanceof ExportDeclaration) {
                ExportDeclaration exportDecl = (ExportDeclaration) item;
                switch (exportDecl.getType()) {
                case All: {
                    String module = exportDecl.getModuleSpecifier();
                    entries.add(new ExportEntry(item, module, "*", null, null));
                    break;
                }
                case External: {
                    String module = exportDecl.getModuleSpecifier();
                    ExportEntriesForModule(exportDecl.getExportClause(), module, entries);
                    break;
                }
                case Local:
                    ExportEntriesForModule(exportDecl.getExportClause(), null, entries);
                    break;
                case Variable:
                    for (Name name : BoundNames(exportDecl.getVariableStatement())) {
                        String id = name.getIdentifier();
                        entries.add(new ExportEntry(item, null, null, id, id));
                    }
                    break;
                case Declaration:
                    for (Name name : BoundNames(exportDecl.getDeclaration())) {
                        String id = name.getIdentifier();
                        entries.add(new ExportEntry(item, null, null, id, id));
                    }
                    break;
                case DefaultHoistableDeclaration: {
                    Name localName = BoundName(exportDecl.getHoistableDeclaration());
                    entries.add(new ExportEntry(item, null, null, localName.getIdentifier(), "default"));
                    break;
                }
                case DefaultClassDeclaration: {
                    Name localName = BoundName(exportDecl.getClassDeclaration());
                    entries.add(new ExportEntry(item, null, null, localName.getIdentifier(), "default"));
                    break;
                }
                case DefaultExpression: {
                    Name localName = BoundName(exportDecl.getExpression().getBinding());
                    entries.add(new ExportEntry(item, null, null, localName.getIdentifier(), "default"));
                    break;
                }
                default:
                    throw new AssertionError();
                }
            }
        }
        return entries;
    }

    /**
     * 15.2.3.6 Static Semantics: ExportEntriesForModule
     * 
     * @param node
     *            the exports clause node
     * @param module
     *            the module name
     * @param entries
     *            the list of export entries
     */
    private static void ExportEntriesForModule(ExportClause node, String module, List<ExportEntry> entries) {
        if (module == null) {
            assert node.getDefaultEntry() == null;
            assert node.getNameSpace() == null;
            for (ExportSpecifier specifier : node.getExports()) {
                String localName = specifier.getSourceName();
                String importName = null;
                String exportName = specifier.getExportName();
                entries.add(new ExportEntry(specifier, module, importName, localName, exportName));
            }
        } else {
            IdentifierName defaultEntry = node.getDefaultEntry();
            if (defaultEntry != null) {
                String exportName = defaultEntry.getName();
                entries.add(new ExportEntry(defaultEntry, module, "default", null, exportName));
            }
            IdentifierName nameSpace = node.getNameSpace();
            if (nameSpace != null) {
                String exportName = nameSpace.getName();
                entries.add(new ExportEntry(nameSpace, module, "*", null, exportName));
            }
            for (ExportSpecifier specifier : node.getExports()) {
                String localName = null;
                String importName = specifier.getSourceName();
                String exportName = specifier.getExportName();
                entries.add(new ExportEntry(specifier, module, importName, localName, exportName));
            }
        }
    }

    /**
     * 15.2.1.8 Static Semantics: ImportEntries<br>
     * 15.2.2.3 Static Semantics: ImportEntries
     * 
     * @param node
     *            the module node
     * @return the list of import entries
     */
    public static List<ImportEntry> ImportEntries(Module node) {
        ArrayList<ImportEntry> entries = new ArrayList<>();
        for (ModuleItem item : node.getStatements()) {
            if (item instanceof ImportDeclaration) {
                ImportDeclaration importDecl = (ImportDeclaration) item;
                switch (importDecl.getType()) {
                case ImportFrom: {
                    String module = importDecl.getModuleSpecifier();
                    ImportEntriesForModule(importDecl.getImportClause(), module, entries);
                    break;
                }
                case ImportModule:
                    /* empty */
                    break;
                default:
                    throw new AssertionError();
                }
            }
        }
        return entries;
    }

    /**
     * 15.2.2.4 Static Semantics: ImportEntriesForModule
     * 
     * @param node
     *            the import clause node
     * @param module
     *            the module name
     * @param entries
     *            the list of import entries
     */
    private static void ImportEntriesForModule(ImportClause node, String module, List<ImportEntry> entries) {
        BindingIdentifier defaultEntry = node.getDefaultEntry();
        if (defaultEntry != null) {
            String localName = defaultEntry.getName().getIdentifier();
            entries.add(new ImportEntry(defaultEntry, module, "default", localName));
        }
        BindingIdentifier nameSpace = node.getNameSpace();
        if (nameSpace != null) {
            String localName = nameSpace.getName().getIdentifier();
            entries.add(new ImportEntry(nameSpace, module, "*", localName));
        }
        for (ImportSpecifier specifier : node.getNamedImports()) {
            String importName = specifier.getImportName();
            String localName = specifier.getLocalName().getName().getIdentifier();
            entries.add(new ImportEntry(specifier, module, importName, localName));
        }
    }

    /**
     * 15.2.1.9 Static Semantics: ImportedLocalNames ( importEntries )
     * 
     * @param importEntries
     *            the list of import entry records
     * @return the list of local names
     */
    public static Map<String, ImportEntry> ImportedLocalNames(List<ImportEntry> importEntries) {
        HashMap<String, ImportEntry> localNames = new HashMap<>();
        for (ImportEntry importEntry : importEntries) {
            localNames.put(importEntry.getLocalName(), importEntry);
        }
        return localNames;
    }

    /**
     * 15.2.1.10 Static Semantics: ModuleRequests<br>
     * 15.2.2.5 Static Semantics: ModuleRequests<br>
     * 15.2.3.9 Static Semantics: ModuleRequests
     * 
     * @param node
     *            the module node
     * @return the ordered set of module requests
     */
    public static Set<String> ModuleRequests(Module node) {
        LinkedHashSet<String> requests = new LinkedHashSet<>();
        for (ModuleItem item : node.getStatements()) {
            String moduleSpecifier;
            if (item instanceof ExportDeclaration) {
                ExportDeclaration export = (ExportDeclaration) item;
                switch (export.getType()) {
                case All:
                case External:
                    moduleSpecifier = export.getModuleSpecifier();
                    break;
                case Local:
                case Variable:
                case Declaration:
                case DefaultHoistableDeclaration:
                case DefaultClassDeclaration:
                case DefaultExpression:
                    continue;
                default:
                    throw new AssertionError();
                }
            } else if (item instanceof ImportDeclaration) {
                moduleSpecifier = ((ImportDeclaration) item).getModuleSpecifier();
            } else {
                continue;
            }
            assert moduleSpecifier != null;
            requests.add(moduleSpecifier);
        }
        return requests;
    }

    /**
     * 15.2.1.11 Static Semantics: LexicallyDeclaredNames
     * 
     * @param node
     *            the module node
     * @return the set of lexically declared names
     */
    public static Set<Name> LexicallyDeclaredNames(Module node) {
        return node.getScope().lexicallyDeclaredNames();
    }

    /**
     * 15.2.1.12 Static Semantics: LexicallyScopedDeclarations
     * 
     * @param node
     *            the module node
     * @return the list of lexically scoped declarations
     */
    public static List<Declaration> LexicallyScopedDeclarations(Module node) {
        return node.getScope().lexicallyScopedDeclarations();
    }

    /**
     * 15.2.1.13 Static Semantics: VarDeclaredNames
     * 
     * @param node
     *            the module node
     * @return the set of variable declared names
     */
    public static Set<Name> VarDeclaredNames(Module node) {
        return node.getScope().varDeclaredNames();
    }

    /**
     * 15.2.1.14 Static Semantics: VarScopedDeclarations
     * 
     * @param node
     *            the module node
     * @return the list of variable scoped declarations
     */
    public static List<StatementListItem> VarScopedDeclarations(Module node) {
        return node.getScope().varScopedDeclarations();
    }

    /**
     * 12.2.5.6 Static Semantics: PropName<br>
     * 14.3.6 Static Semantics: PropName
     * 
     * @param node
     *            the property definition
     * @return the property name string
     */
    public static String PropName(PropertyDefinition node) {
        return node.getPropertyName().getName();
    }

    /**
     * 12.2.5.6 Static Semantics: PropName
     * 
     * @param node
     *            the property name node
     * @return the property name string
     */
    public static String PropName(PropertyName node) {
        return node.getName();
    }

    /**
     * 14.3.8 Static Semantics: SpecialMethod
     * 
     * @param node
     *            the method node
     * @return {@code true} if <var>node</var> is not a simple function
     */
    public static boolean SpecialMethod(MethodDefinition node) {
        switch (node.getType()) {
        case AsyncFunction:
        case AsyncGenerator:
        case Generator:
        case Getter:
        case Setter:
            return true;
        case CallConstructor:
        case ClassConstructor:
        case Function:
            return false;
        default:
            throw new AssertionError();
        }
    }

    /**
     * Returns the list of decorated methods.
     * 
     * @param properties
     *            the list of properties
     * @return the list of decorated methods
     */
    public static List<MethodDefinition> DecoratedMethods(List<? extends PropertyDefinition> properties) {
        InlineArrayList<MethodDefinition> decoratedMethods = new InlineArrayList<>();
        DecoratedMethods(properties, decoratedMethods::add);
        return decoratedMethods;
    }

    /**
     * Applies the given consumer on each decorated method.
     * 
     * @param properties
     *            the list of properties
     * @param consumer
     *            the consumer to apply on decorated methods
     */
    public static void DecoratedMethods(List<? extends PropertyDefinition> properties,
            Consumer<MethodDefinition> consumer) {
        for (PropertyDefinition property : properties) {
            if (property instanceof MethodDefinition) {
                MethodDefinition method = (MethodDefinition) property;
                if (!method.getDecorators().isEmpty()) {
                    consumer.accept(method);
                }
            } else if (property instanceof PropertyDefinitionsMethod) {
                DecoratedMethods(((PropertyDefinitionsMethod) property).getProperties(), consumer);
            } else if (property instanceof MethodDefinitionsMethod) {
                DecoratedMethods(((MethodDefinitionsMethod) property).getProperties(), consumer);
            }
        }
    }

    /**
     * Tests if {@code node} has any instance fields or methods.
     * 
     * @param node
     *            the class definition
     * @return {@code true} if the class has any instance fields or methods
     */
    public static boolean HasClassInstanceDefinitions(ClassDefinition node) {
        return ClassProperties(node.getProperties()).anyMatch(p -> {
            assert p instanceof MethodDefinition || p instanceof ClassFieldDefinition;
            if (p instanceof ClassFieldDefinition) {
                return !((ClassFieldDefinition) p).isStatic();
            }
            MethodDefinition m = (MethodDefinition) p;
            return !m.isStatic() && m.getClassElementName() instanceof PrivateNameProperty;
        });
    }

    /**
     * Returns the flattened list of all instance class properties.
     * 
     * @param properties
     *            the list of properties
     * @return the list of instance class properties
     */
    public static Stream<PropertyDefinition> ClassProperties(List<PropertyDefinition> properties) {
        return properties.stream().flatMap(p -> {
            if (p instanceof MethodDefinitionsMethod) {
                return ClassProperties(((MethodDefinitionsMethod) p).getProperties());
            }
            assert p instanceof MethodDefinition || p instanceof ClassFieldDefinition;
            return Stream.of(p);
        });
    }

    /**
     * Returns the list of private bound names.
     * 
     * @param node
     *            the class definition
     * @return the list of private bound names
     */
    public static List<Name> PrivateBoundNames(ClassDefinition node) {
        InlineArrayList<Name> list = new InlineArrayList<>();
        PrivateBoundNames(node.getProperties(), list);
        return list;
    }

    private static void PrivateBoundNames(List<PropertyDefinition> properties, InlineArrayList<Name> list) {
        for (PropertyDefinition property : properties) {
            if (property instanceof MethodDefinition) {
                ClassElementName classElementName = ((MethodDefinition) property).getClassElementName();
                if (classElementName instanceof PrivateNameProperty) {
                    list.add(((PrivateNameProperty) classElementName).getName());
                }
            } else if (property instanceof ClassFieldDefinition) {
                ClassElementName classElementName = ((ClassFieldDefinition) property).getClassElementName();
                if (classElementName instanceof PrivateNameProperty) {
                    list.add(((PrivateNameProperty) classElementName).getName());
                }
            } else if (property instanceof MethodDefinitionsMethod) {
                PrivateBoundNames(((MethodDefinitionsMethod) property).getProperties(), list);
            }
        }
    }

    /**
     * 12.2.8.1 Static Semantics: TemplateStrings
     * 
     * @param node
     *            the template literal
     * @return the list of template literal strings
     */
    public static List<TemplateCharacters> TemplateStrings(TemplateLiteral node) {
        List<Expression> elements = node.getElements();
        assert (elements.size() & 1) == 1;
        int numChars = ((elements.size() / 2) + 1);
        ArrayList<TemplateCharacters> strings = new ArrayList<>(numChars);
        for (int i = 0, size = elements.size(); i < size; ++i) {
            if ((i & 1) == 1) {
                assert !(elements.get(i) instanceof TemplateCharacters);
                continue;
            }
            strings.add((TemplateCharacters) elements.get(i));
        }
        return strings;
    }

    /**
     * Static Semantics: Substitutions (not in spec)
     * 
     * @param node
     *            the template literal
     * @return the list of template literal substitutions
     */
    public static List<Expression> Substitutions(TemplateLiteral node) {
        List<Expression> elements = node.getElements();
        assert (elements.size() & 1) == 1;
        int numSubst = (elements.size() / 2);
        ArrayList<Expression> substitutions = new ArrayList<>(numSubst);
        for (int i = 0, size = elements.size(); i < size; ++i) {
            if ((i & 1) == 0) {
                assert (elements.get(i) instanceof TemplateCharacters);
                continue;
            }
            substitutions.add(elements.get(i));
        }
        return substitutions;
    }

    /**
     * Static Semantics: TailCallNodes (not in spec)
     * <p>
     * 14.6 Tail Position Calls
     * <ul>
     * <li>14.6.1 Static Semantics: IsInTailPosition(nonterminal)
     * <li>14.6.2 Static Semantics: HasProductionInTailPosition
     * </ul>
     * 
     * @param expr
     *            the return expression
     * @return the set of tail call nodes
     */
    public static Set<Expression> TailCallNodes(Expression expr) {
        expr = peelCommaExpression(expr);
        if (IsTailCallExpression(expr)) {
            return singleton(expr);
        }
        if (expr instanceof ConditionalExpression || IsLogicalExpression(expr)) {
            HashSet<Expression> tail = new HashSet<>(8);
            for (ArrayDeque<Expression> queue = new ArrayDeque<>(singleton(expr)); !queue.isEmpty();) {
                Expression e = peelCommaExpression(queue.remove());
                if (IsTailCallExpression(e)) {
                    tail.add(e);
                } else if (e instanceof ConditionalExpression) {
                    queue.add(((ConditionalExpression) e).getThen());
                    queue.add(((ConditionalExpression) e).getOtherwise());
                } else if (IsLogicalExpression(e)) {
                    queue.add(((BinaryExpression) e).getRight());
                }
            }
            return tail;
        }
        return emptySet();
    }

    private static Expression peelCommaExpression(Expression expr) {
        while (expr instanceof CommaExpression) {
            expr = last(((CommaExpression) expr).getOperands());
        }
        return expr;
    }

    private static boolean IsLogicalExpression(Expression expr) {
        if (expr instanceof BinaryExpression) {
            BinaryExpression.Operator op = ((BinaryExpression) expr).getOperator();
            return op == BinaryExpression.Operator.AND || op == BinaryExpression.Operator.OR;
        }
        return false;
    }

    private static boolean IsTailCallExpression(Expression expr) {
        return expr instanceof CallExpression || expr instanceof TemplateCallExpression;
    }

    private static <T> T last(List<T> list) {
        assert !list.isEmpty();
        return list.get(list.size() - 1);
    }
}
