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

import static com.github.anba.es6draft.compiler.DefaultCodeGenerator.SetFunctionName;
import static com.github.anba.es6draft.compiler.DefaultCodeGenerator.ToPropertyKey;
import static com.github.anba.es6draft.semantics.StaticSemantics.IsAnonymousFunctionDefinition;
import static com.github.anba.es6draft.semantics.StaticSemantics.IsIdentifierRef;
import static com.github.anba.es6draft.semantics.StaticSemantics.PropName;

import java.util.HashSet;
import java.util.List;

import com.github.anba.es6draft.ast.*;
import com.github.anba.es6draft.compiler.DefaultCodeGenerator.ValType;
import com.github.anba.es6draft.compiler.Labels.TempLabel;
import com.github.anba.es6draft.compiler.StatementGenerator.Completion;
import com.github.anba.es6draft.compiler.assembler.FieldName;
import com.github.anba.es6draft.compiler.assembler.Jump;
import com.github.anba.es6draft.compiler.assembler.MethodName;
import com.github.anba.es6draft.compiler.assembler.MutableValue;
import com.github.anba.es6draft.compiler.assembler.Type;
import com.github.anba.es6draft.compiler.assembler.Value;
import com.github.anba.es6draft.compiler.assembler.Variable;
import com.github.anba.es6draft.runtime.internal.IndexedMap;
import com.github.anba.es6draft.runtime.internal.ScriptIterator;

/**
 * <h1>12 ECMAScript Language: Expressions</h1><br>
 * <h2>12.14 Assignment Operators</h2>
 * <ul>
 * <li>12.14.5 Destructuring Assignment
 * </ul>
 */
final class DestructuringAssignmentGenerator {
    private static final class Fields {
        static final FieldName Collections_EMPTY_SET = FieldName.findStatic(Types.Collections, "EMPTY_SET", Types.Set);
    }

    private static final class Methods {
        // class: AbstractOperations
        static final MethodName AbstractOperations_GetV = MethodName.findStatic(Types.AbstractOperations, "GetV",
                Type.methodType(Types.Object, Types.ExecutionContext, Types.Object, Types.Object));

        static final MethodName AbstractOperations_GetV_long = MethodName.findStatic(Types.AbstractOperations, "GetV",
                Type.methodType(Types.Object, Types.ExecutionContext, Types.Object, Type.LONG_TYPE));

        static final MethodName AbstractOperations_GetV_String = MethodName.findStatic(Types.AbstractOperations, "GetV",
                Type.methodType(Types.Object, Types.ExecutionContext, Types.Object, Types.String));

        static final MethodName AbstractOperations_RequireObjectCoercible = MethodName.findStatic(
                Types.AbstractOperations, "RequireObjectCoercible",
                Type.methodType(Types.Object, Types.ExecutionContext, Types.Object));

        // class: ArrayOperations
        static final MethodName ArrayOperations_createRestArray = MethodName.findStatic(Types.ArrayOperations,
                "createRestArray", Type.methodType(Types.ArrayObject, Types.Iterator, Types.ExecutionContext));

        // class: ObjectOperations
        static final MethodName ObjectOperations_createRestObject = MethodName.findStatic(Types.ObjectOperations,
                "createRestObject",
                Type.methodType(Types.OrdinaryObject, Types.Object, Types.Set, Types.ExecutionContext));

        // class: IteratorOperations
        static final MethodName IteratorOperations_iterate = MethodName.findStatic(Types.IteratorOperations, "iterate",
                Type.methodType(Types.ScriptIterator, Types.Object, Types.ExecutionContext));

        static final MethodName IteratorOperations_iteratorNextAndIgnore = MethodName.findStatic(
                Types.IteratorOperations, "iteratorNextAndIgnore", Type.methodType(Type.VOID_TYPE, Types.Iterator));

        static final MethodName IteratorOperations_iteratorNextOrUndefined = MethodName.findStatic(
                Types.IteratorOperations, "iteratorNextOrUndefined", Type.methodType(Types.Object, Types.Iterator));

        // class: HashSet
        static final MethodName HashSet_new = MethodName.findConstructor(Types.HashSet,
                Type.methodType(Type.VOID_TYPE));

        static final MethodName HashSet_add = MethodName.findVirtual(Types.HashSet, "add",
                Type.methodType(Type.BOOLEAN_TYPE, Types.Object));
    }

    private DestructuringAssignmentGenerator() {
    }

    /**
     * stack: [value] {@literal ->} []
     * 
     * @param codegen
     *            the code generator
     * @param node
     *            the assignment pattern node
     * @param mv
     *            the code visitor
     */
    static void DestructuringAssignment(CodeGenerator codegen, AssignmentPattern node, CodeVisitor mv) {
        DestructuringAssignmentEvaluation init = new DestructuringAssignmentEvaluation(codegen, mv);
        node.accept(init, null);
    }

    private static abstract class RuntimeSemantics<V> extends DefaultVoidNodeVisitor<V> {
        protected final CodeGenerator codegen;
        protected final CodeVisitor mv;

        RuntimeSemantics(CodeGenerator codegen, CodeVisitor mv) {
            this.codegen = codegen;
            this.mv = mv;
        }

        protected final void DestructuringAssignmentEvaluation(AssignmentPattern node) {
            node.accept(new DestructuringAssignmentEvaluation(codegen, mv), null);
        }

        protected final void IteratorDestructuringAssignmentEvaluation(AssignmentElementItem node,
                Variable<ScriptIterator<?>> iterator) {
            node.accept(new IteratorDestructuringAssignmentEvaluation(codegen, mv), iterator);
        }

        protected final void KeyedDestructuringAssignmentEvaluation(AssignmentProperty node, String key,
                Variable<Object> value, Variable<HashSet<?>> propertyNames) {
            node.accept(new LiteralKeyedDestructuringAssignmentEvaluation(codegen, mv, value, propertyNames), key);
        }

        protected final void KeyedDestructuringAssignmentEvaluation(AssignmentProperty node, ComputedPropertyName key,
                Variable<Object> value, Variable<HashSet<?>> propertyNames) {
            node.accept(new ComputedKeyedDestructuringAssignmentEvaluation(codegen, mv, value, propertyNames), key);
        }

        protected final ValType expression(Expression node, CodeVisitor mv) {
            return codegen.expression(node, mv);
        }

        protected final ValType expressionBoxed(Expression node, CodeVisitor mv) {
            return codegen.expressionBoxed(node, mv);
        }

        @Override
        protected final void visit(Node node, V value) {
            throw new IllegalStateException();
        }
    }

    /**
     * 12.14.5.2 Runtime Semantics: DestructuringAssignmentEvaluation
     */
    private static final class DestructuringAssignmentEvaluation extends RuntimeSemantics<Void> {
        DestructuringAssignmentEvaluation(CodeGenerator codegen, CodeVisitor mv) {
            super(codegen, mv);
        }

        @Override
        public void visit(ArrayAssignmentPattern node, Void value) {
            // stack: [value] -> []
            mv.enterVariableScope();
            Variable<ScriptIterator<?>> iterator = mv.newVariable("iterator", ScriptIterator.class).uncheckedCast();
            mv.loadExecutionContext();
            mv.lineInfo(node);
            mv.invoke(Methods.IteratorOperations_iterate);
            mv.store(iterator);

            new IterationGenerator<ArrayAssignmentPattern>(codegen) {
                @Override
                protected Completion iterationBody(ArrayAssignmentPattern node, Variable<ScriptIterator<?>> iterator,
                        CodeVisitor mv) {
                    for (AssignmentElementItem element : node.getElements()) {
                        IteratorDestructuringAssignmentEvaluation(element, iterator);
                    }
                    return Completion.Normal;
                }

                @Override
                protected void epilogue(ArrayAssignmentPattern node, Variable<ScriptIterator<?>> iterator,
                        CodeVisitor mv) {
                    IteratorClose(node, iterator, mv);
                }

                @Override
                protected MutableValue<Object> enterIteration(ArrayAssignmentPattern node, CodeVisitor mv) {
                    return mv.enterIteration();
                }

                @Override
                protected List<TempLabel> exitIteration(ArrayAssignmentPattern node, CodeVisitor mv) {
                    return mv.exitIteration();
                }
            }.generate(node, iterator, mv);

            mv.exitVariableScope();
        }

        @Override
        public void visit(ObjectAssignmentPattern node, Void value) {
            // stack: [value] -> [value]
            mv.loadExecutionContext();
            mv.swap();
            mv.lineInfo(node);
            mv.invoke(Methods.AbstractOperations_RequireObjectCoercible);

            // ObjectAssignmentPattern : { }
            if (node.getProperties().isEmpty() && node.getRest() == null) {
                // stack: [value] -> []
                mv.pop();
                return;
            }

            // stack: [value] -> []
            mv.enterVariableScope();
            Variable<Object> val = mv.newVariable("value", Object.class);
            mv.store(val);

            Variable<HashSet<?>> propertyNames = null;
            if (!node.getProperties().isEmpty() && node.getRest() != null) {
                propertyNames = mv.newVariable("propertyNames", HashSet.class).uncheckedCast();
                mv.anew(Methods.HashSet_new);
                mv.store(propertyNames);
            }

            // ObjectAssignmentPattern : { AssignmentPropertyList }
            for (AssignmentProperty property : node.getProperties()) {
                if (property.getPropertyName() == null) {
                    // AssignmentProperty : IdentifierReference Initializer{opt}
                    assert property.getTarget() instanceof IdentifierReference;
                    String name = ((IdentifierReference) property.getTarget()).getName();
                    KeyedDestructuringAssignmentEvaluation(property, name, val, propertyNames);
                } else {
                    // AssignmentProperty : PropertyName : AssignmentElement
                    String name = PropName(property.getPropertyName());
                    if (name != null) {
                        KeyedDestructuringAssignmentEvaluation(property, name, val, propertyNames);
                    } else {
                        PropertyName propertyName = property.getPropertyName();
                        assert propertyName instanceof ComputedPropertyName;
                        KeyedDestructuringAssignmentEvaluation(property, (ComputedPropertyName) propertyName, val,
                                propertyNames);
                    }
                }
            }

            // ObjectAssignmentPattern : { ... DestructuringAssignmentTarget }
            // ObjectAssignmentPattern: { AssignmentPropertyList , ... DestructuringAssignmentTarget }
            AssignmentRestProperty rest = node.getRest();
            if (rest != null) {
                LeftHandSideExpression target = rest.getTarget();
                ReferenceOp<LeftHandSideExpression> op = null;

                /* steps 1, 1-2 (not applicable) */
                /* step 2, 3 */
                ValType refType = null;
                if (!(target instanceof AssignmentPattern)) {
                    // stack: [] -> [lref]
                    op = ReferenceOp.of(target);
                    refType = op.reference(target, mv, codegen);
                }

                /* steps 3-5, 4-6 */
                // stack: [lref?] -> [lref?, restObj]
                mv.load(val);
                if (propertyNames != null) {
                    mv.load(propertyNames);
                } else {
                    mv.get(Fields.Collections_EMPTY_SET);
                }
                mv.loadExecutionContext();
                mv.lineInfo(rest);
                mv.invoke(Methods.ObjectOperations_createRestObject);

                // Exit the variable scope early to avoid restoring the value/propertyNames slots after a yield.
                mv.exitVariableScope();

                /* steps 6-8, 7-9 */
                if (!(target instanceof AssignmentPattern)) {
                    /* step 6, 7 */
                    // stack: [lref, restObj] -> []
                    op.putValue(target, refType, ValType.Object, mv);
                } else {
                    /* steps 7-8, 8-9 */
                    // stack: [restObj] -> []
                    DestructuringAssignmentEvaluation((AssignmentPattern) target);
                }
            } else {
                mv.exitVariableScope();
            }
        }
    }

    /**
     * 12.14.5.3 Runtime Semantics: IteratorDestructuringAssignmentEvaluation
     */
    private static final class IteratorDestructuringAssignmentEvaluation
            extends RuntimeSemantics<Variable<ScriptIterator<?>>> {
        IteratorDestructuringAssignmentEvaluation(CodeGenerator codegen, CodeVisitor mv) {
            super(codegen, mv);
        }

        @Override
        public void visit(Elision node, Variable<ScriptIterator<?>> iterator) {
            // stack: [] -> []
            mv.load(iterator);
            mv.lineInfo(node);
            mv.invoke(Methods.IteratorOperations_iteratorNextAndIgnore);
        }

        @Override
        public void visit(AssignmentElement node, Variable<ScriptIterator<?>> iterator) {
            LeftHandSideExpression target = node.getTarget();
            Expression initializer = node.getInitializer();
            ReferenceOp<LeftHandSideExpression> op = null;

            /* step 1 */
            ValType refType = null;
            if (!(target instanceof AssignmentPattern)) {
                // stack: [] -> [lref]
                op = ReferenceOp.of(target);
                refType = op.reference(target, mv, codegen);
            }

            /* steps 2-3 */
            // stack: [(lref)] -> [(lref), v]
            mv.load(iterator);
            mv.lineInfo(node);
            mv.invoke(Methods.IteratorOperations_iteratorNextOrUndefined);

            /* steps 4-5 */
            // stack: [(lref), v] -> [(lref), v']
            if (initializer != null) {
                Jump undef = new Jump();
                mv.dup();
                mv.loadUndefined();
                mv.ifacmpne(undef);
                {
                    mv.pop();
                    expressionBoxed(initializer, mv);
                    /* step 7 (moved) */
                    if (IsAnonymousFunctionDefinition(initializer) && IsIdentifierRef(target)) {
                        SetFunctionName(initializer, ((IdentifierReference) target).getName(), mv);
                    }
                }
                mv.mark(undef);
            }

            /* steps 6-8 */
            if (target instanceof AssignmentPattern) {
                // stack: [v'] -> []
                DestructuringAssignmentEvaluation((AssignmentPattern) target);
            } else {
                // stack: [lref, 'v] -> []
                op.putValue(target, refType, ValType.Any, mv);
            }
        }

        @Override
        public void visit(AssignmentRestElement node, Variable<ScriptIterator<?>> iterator) {
            LeftHandSideExpression target = node.getTarget();
            ReferenceOp<LeftHandSideExpression> op = null;

            /* step 1 */
            ValType refType = null;
            if (!(target instanceof AssignmentPattern)) {
                // stack: [] -> [lref]
                op = ReferenceOp.of(target);
                refType = op.reference(target, mv, codegen);
            }

            /* steps 2-4 */
            // stack: [(lref)] -> [(lref), rest]
            mv.load(iterator);
            mv.loadExecutionContext();
            mv.lineInfo(node);
            mv.invoke(Methods.ArrayOperations_createRestArray);

            /* steps 5-7 */
            if (!(target instanceof AssignmentPattern)) {
                // stack: [lref, rest] -> []
                op.putValue(target, refType, ValType.Object, mv);
            } else {
                // stack: [rest] -> []
                DestructuringAssignmentEvaluation((AssignmentPattern) target);
            }
        }
    }

    /**
     * 12.14.5.4 Runtime Semantics: KeyedDestructuringAssignmentEvaluation
     */
    private static abstract class KeyedDestructuringAssignmentEvaluation<PROPERTYNAME>
            extends RuntimeSemantics<PROPERTYNAME> {
        private final Variable<Object> value;

        KeyedDestructuringAssignmentEvaluation(CodeGenerator codegen, CodeVisitor mv, Variable<Object> value) {
            super(codegen, mv);
            this.value = value;
        }

        abstract ValType evaluatePropertyName(PROPERTYNAME propertyName);

        abstract boolean isSimplePropertyName(PROPERTYNAME propertyName);

        final boolean isSimplePropertyNameOrTarget(LeftHandSideExpression target, PROPERTYNAME propertyName) {
            if (isSimplePropertyName(propertyName)) {
                return true;
            }
            return false;
        }

        final void emitGetV(AssignmentProperty node, ValType type) {
            // stack: [cx, value, propertyName] -> [v]
            mv.lineInfo(node);
            switch (type) {
            case Number_uint:
                mv.invoke(Methods.AbstractOperations_GetV_long);
                break;
            case String:
                mv.invoke(Methods.AbstractOperations_GetV_String);
                break;
            case Any:
                mv.invoke(Methods.AbstractOperations_GetV);
                break;
            default:
                throw new AssertionError();
            }
        }

        @Override
        public void visit(AssignmentProperty node, PROPERTYNAME propertyName) {
            LeftHandSideExpression target = node.getTarget();
            Expression initializer = node.getInitializer();
            ReferenceOp<LeftHandSideExpression> op;

            ValType type, refType;
            if (target instanceof AssignmentPattern) {
                /* step 1 (not applicable) */
                op = null;
                refType = null;

                // stack: [] -> [cx, value]
                mv.loadExecutionContext();
                mv.load(value);

                // stack: [cx, value] -> [cx, value, propertyName]
                type = evaluatePropertyName(propertyName);
            } else if (isSimplePropertyNameOrTarget(target, propertyName)) {
                /* step 1 */
                // stack: [] -> [lref]
                op = ReferenceOp.of(target);
                refType = op.reference(target, mv, codegen);

                // stack: [lref] -> [lref, cx, value]
                mv.loadExecutionContext();
                mv.load(value);

                // stack: [lref, cx, value] -> [lref, cx, value, propertyName]
                type = evaluatePropertyName(propertyName);
            } else {
                // stack: [] -> []
                type = evaluatePropertyName(propertyName);
                Value<?> propertyNameVar = mv.storeTemporary(type.toClass());

                /* step 1 */
                // stack: [] -> [lref]
                op = ReferenceOp.of(target);
                refType = op.reference(target, mv, codegen);

                // stack: [lref] -> [lref, cx, value, propertyName]
                mv.loadExecutionContext();
                mv.load(value);
                mv.load(propertyNameVar);
            }

            /* steps 2-3 */
            // stack: [(lref), cx, value, propertyName] -> [(lref), v]
            emitGetV(node, type);

            /* steps 4-5 */
            // stack: [(lref), v] -> [(lref), v']
            if (initializer != null) {
                Jump undef = new Jump();
                mv.dup();
                mv.loadUndefined();
                mv.ifacmpne(undef);
                {
                    mv.pop();
                    expressionBoxed(initializer, mv);
                    /* step 7 (moved) */
                    if (IsAnonymousFunctionDefinition(initializer) && IsIdentifierRef(target)) {
                        SetFunctionName(initializer, ((IdentifierReference) target).getName(), mv);
                    }
                }
                mv.mark(undef);
            }

            /* steps 6-8 */
            if (target instanceof AssignmentPattern) {
                // stack: [v'] -> []
                DestructuringAssignmentEvaluation((AssignmentPattern) target);
            } else {
                // stack: [lref, 'v] -> []
                op.putValue(target, refType, ValType.Any, mv);
            }
        }
    }

    /**
     * 12.14.5.4 Runtime Semantics: KeyedDestructuringAssignmentEvaluation
     */
    private static final class LiteralKeyedDestructuringAssignmentEvaluation
            extends KeyedDestructuringAssignmentEvaluation<String> {
        private final Variable<HashSet<?>> propertyNames;

        LiteralKeyedDestructuringAssignmentEvaluation(CodeGenerator codegen, CodeVisitor mv, Variable<Object> value,
                Variable<HashSet<?>> propertyNames) {
            super(codegen, mv, value);
            this.propertyNames = propertyNames;
        }

        @Override
        ValType evaluatePropertyName(String propertyName) {
            if (propertyNames != null) {
                mv.load(propertyNames);
                mv.aconst(propertyName);
                mv.invoke(Methods.HashSet_add);
                mv.pop();
            }
            long index = IndexedMap.toIndex(propertyName);
            if (IndexedMap.isIndex(index)) {
                mv.lconst(index);
                return ValType.Number_uint;
            }
            mv.aconst(propertyName);
            return ValType.String;
        }

        @Override
        boolean isSimplePropertyName(String propertyName) {
            return true;
        }
    }

    /**
     * 12.14.5.4 Runtime Semantics: KeyedDestructuringAssignmentEvaluation
     */
    private static final class ComputedKeyedDestructuringAssignmentEvaluation
            extends KeyedDestructuringAssignmentEvaluation<ComputedPropertyName> {
        private final Variable<HashSet<?>> propertyNames;

        ComputedKeyedDestructuringAssignmentEvaluation(CodeGenerator codegen, CodeVisitor mv, Variable<Object> value,
                Variable<HashSet<?>> propertyNames) {
            super(codegen, mv, value);
            this.propertyNames = propertyNames;
        }

        @Override
        ValType evaluatePropertyName(ComputedPropertyName propertyName) {
            if (propertyNames != null) {
                mv.load(propertyNames);
            }
            // Runtime Semantics: Evaluation
            // ComputedPropertyName : [ AssignmentExpression ]
            ValType propType = expression(propertyName.getExpression(), mv);
            ToPropertyKey(propType, mv);
            if (propertyNames != null) {
                mv.dupX1();
                mv.invoke(Methods.HashSet_add);
                mv.pop();
            }
            return ValType.Any;
        }

        @Override
        boolean isSimplePropertyName(ComputedPropertyName propertyName) {
            return propertyName.getExpression() instanceof Literal;
        }
    }
}
