/**
 * Copyright (c) 2012-2016 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 com.github.anba.es6draft.ast.Script;
import com.github.anba.es6draft.compiler.CodeGenerator.ScriptName;
import com.github.anba.es6draft.compiler.assembler.Code.MethodCode;
import com.github.anba.es6draft.compiler.assembler.MethodName;
import com.github.anba.es6draft.compiler.assembler.TryCatchLabel;
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.DeclarativeEnvironmentRecord;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.LexicalEnvironment;
import com.github.anba.es6draft.runtime.Realm;

/**
 * Generates bytecode for the script entry method.
 */
final class ScriptCodeGenerator {
    private static final class Methods {
        // ExecutionContext
        static final MethodName ExecutionContext_newEvalExecutionContext = MethodName.findStatic(
                Types.ExecutionContext, "newEvalExecutionContext",
                Type.methodType(Types.ExecutionContext, Types.ExecutionContext, Types.Script,
                        Types.LexicalEnvironment, Types.LexicalEnvironment));

        static final MethodName ExecutionContext_newScriptExecutionContext = MethodName.findStatic(
                Types.ExecutionContext, "newScriptExecutionContext",
                Type.methodType(Types.ExecutionContext, Types.Realm, Types.Script));

        static final MethodName ExecutionContext_getLexicalEnvironment = MethodName.findVirtual(
                Types.ExecutionContext, "getLexicalEnvironment",
                Type.methodType(Types.LexicalEnvironment));

        static final MethodName ExecutionContext_setLexicalEnvironment = MethodName.findVirtual(
                Types.ExecutionContext, "setLexicalEnvironment",
                Type.methodType(Type.VOID_TYPE, Types.LexicalEnvironment));

        static final MethodName ExecutionContext_getVariableEnvironment = MethodName.findVirtual(
                Types.ExecutionContext, "getVariableEnvironment",
                Type.methodType(Types.LexicalEnvironment));

        static final MethodName ExecutionContext_getRealm = MethodName
                .findVirtual(Types.ExecutionContext, "getRealm", Type.methodType(Types.Realm));

        // class: LexicalEnvironment
        static final MethodName LexicalEnvironment_newDeclarativeEnvironment = MethodName
                .findStatic(Types.LexicalEnvironment, "newDeclarativeEnvironment", Type.methodType(
                        Types.LexicalEnvironment, Types.LexicalEnvironment));

        // Realm
        static final MethodName Realm_getGlobalEnv = MethodName.findVirtual(Types.Realm,
                "getGlobalEnv", Type.methodType(Types.LexicalEnvironment));

        static final MethodName Realm_getScriptContext = MethodName.findVirtual(Types.Realm,
                "getScriptContext", Type.methodType(Types.ExecutionContext));

        static final MethodName Realm_setScriptContext = MethodName.findVirtual(Types.Realm,
                "setScriptContext", Type.methodType(Type.VOID_TYPE, Types.ExecutionContext));
    }

    private static final int EXECUTION_CONTEXT = 0;
    private static final int SCRIPT = 1;

    private static final class ScriptEvalMethodGenerator extends InstructionVisitor {
        ScriptEvalMethodGenerator(MethodCode method) {
            super(method);
        }

        @Override
        public void begin() {
            super.begin();
            setParameterName("callerContext", EXECUTION_CONTEXT, Types.ExecutionContext);
            setParameterName("script", SCRIPT, Types.Script);
        }
    }

    private final CodeGenerator codegen;

    ScriptCodeGenerator(CodeGenerator codegen) {
        this.codegen = codegen;
    }

    void generate(Script node) {
        InstructionVisitor mv = new ScriptEvalMethodGenerator(
                codegen.newMethod(node, ScriptName.Eval));
        mv.lineInfo(node);
        mv.begin();
        if (node.isScripting()) {
            generateScriptingEvaluation(node, mv);
        } else if (node.isEvalScript()) {
            generateEvalScriptEvaluation(node, mv);
        } else {
            generateGlobalScriptEvaluation(node, mv);
        }
        mv.end();
    }

    /**
     * 15.1.7 Runtime Semantics: ScriptEvaluation
     * 
     * @param node
     *            the script node
     * @param mv
     *            the instruction visitor
     */
    private void generateGlobalScriptEvaluation(Script node, InstructionVisitor mv) {
        Variable<ExecutionContext> callerContext = mv.getParameter(EXECUTION_CONTEXT,
                ExecutionContext.class);
        Variable<com.github.anba.es6draft.Script> script = mv.getParameter(SCRIPT,
                com.github.anba.es6draft.Script.class);
        Variable<Realm> realm = mv.newVariable("realm", Realm.class);
        Variable<ExecutionContext> scriptCxt = mv.newVariable("scriptCxt", ExecutionContext.class);
        Variable<ExecutionContext> oldScriptContext = mv.newVariable("oldScriptContext",
                ExecutionContext.class);
        Variable<Object> result = mv.newVariable("result", Object.class);
        Variable<Throwable> throwable = mv.newVariable("throwable", Throwable.class);

        getRealm(callerContext, realm, mv);
        /* steps 1-2 (not applicable) */
        /* steps 3-7 */
        newScriptExecutionContext(realm, script, scriptCxt, mv);
        /* step 8 */
        getScriptContext(realm, oldScriptContext, mv);
        /* step 9 */
        setScriptContext(realm, scriptCxt, mv);

        TryCatchLabel startFinally = new TryCatchLabel(), endFinally = new TryCatchLabel();
        TryCatchLabel handlerFinally = new TryCatchLabel();
        mv.mark(startFinally);
        {
            /* step 10 */
            mv.load(scriptCxt);
            mv.invoke(codegen.methodDesc(node, ScriptName.Init));
            /* steps 11-12 */
            mv.load(scriptCxt);
            mv.invoke(codegen.methodDesc(node, ScriptName.Code));
            mv.store(result);
            /* steps 13-15  */
            setScriptContext(realm, oldScriptContext, mv);
            /* step 16 */
            mv.load(result);
            mv._return();
        }
        mv.mark(endFinally);

        // Exception: Restore script context and then rethrow exception
        mv.finallyHandler(handlerFinally);
        mv.store(throwable);
        /* steps 13-15 */
        setScriptContext(realm, oldScriptContext, mv);
        mv.load(throwable);
        mv.athrow();

        mv.tryFinally(startFinally, endFinally, handlerFinally);
    }

    /**
     * 18.2.1.1 Runtime Semantics: PerformEval( x, evalRealm, strictCaller, direct)
     * 
     * @param node
     *            the script node
     * @param mv
     *            the instruction visitor
     */
    private void generateEvalScriptEvaluation(Script node, InstructionVisitor mv) {
        Variable<ExecutionContext> callerContext = mv.getParameter(EXECUTION_CONTEXT,
                ExecutionContext.class);
        Variable<com.github.anba.es6draft.Script> script = mv.getParameter(SCRIPT,
                com.github.anba.es6draft.Script.class);
        Variable<ExecutionContext> evalCxt = mv.newVariable("evalCxt", ExecutionContext.class);
        Variable<? extends LexicalEnvironment<?>> varEnv = mv
                .newVariable("varEnv", LexicalEnvironment.class).uncheckedCast();
        Variable<? extends LexicalEnvironment<?>> lexEnv = mv
                .newVariable("lexEnv", LexicalEnvironment.class).uncheckedCast();

        // Optimization: Skip creating lexical environment if no declarations are present.
        boolean noDeclarations = node.getScope().lexicallyDeclaredNames().isEmpty();
        if (node.isStrict()) {
            noDeclarations &= node.getScope().varDeclaredNames().isEmpty();
        }

        /* steps 1-5 (not applicable) */
        /* steps 6-7 */
        boolean strictEval = node.isStrict();
        /* step 8 (omitted) */
        /* steps 9-10 */
        if (node.isDirectEval()) {
            /* step 9 */
            getVariableEnvironment(callerContext, varEnv, mv);
            if (noDeclarations) {
                getLexicalEnvironment(callerContext, lexEnv, mv);
            } else {
                newDeclarativeEnvironment(callerContext, lexEnv, mv);
            }
        } else {
            /* step 10 */
            getGlobalEnv(callerContext, varEnv, mv);
            if (noDeclarations) {
                lexEnv = varEnv;
            } else {
                newDeclarativeEnvironment(varEnv, lexEnv, mv);
            }
        }
        /* step 11 */
        if (strictEval) {
            varEnv = lexEnv;
        }
        /* steps 12-17 */
        newEvalExecutionContext(callerContext, script, varEnv, lexEnv, evalCxt, mv);
        /* step 18 */
        mv.load(evalCxt);
        mv.invoke(codegen.methodDesc(node, ScriptName.Init));
        /* steps 19-23 */
        mv.load(evalCxt);
        mv.invoke(codegen.methodDesc(node, ScriptName.Code));
        mv._return();
    }

    private void generateScriptingEvaluation(Script node, InstructionVisitor mv) {
        Variable<ExecutionContext> context = mv.getParameter(EXECUTION_CONTEXT,
                ExecutionContext.class);
        Variable<LexicalEnvironment<DeclarativeEnvironmentRecord>> lexEnv = mv
                .newVariable("lexEnv", LexicalEnvironment.class).uncheckedCast();

        // Create an empty declarative environment for the lexical bindings.
        newDeclarativeEnvironment(context, lexEnv, mv);

        // Replace the lexical environment component.
        setLexicalEnvironment(context, lexEnv, mv);

        // Create local bindings.
        mv.load(context);
        mv.invoke(codegen.methodDesc(node, ScriptName.Init));

        // Evaluate the actual script code.
        mv.load(context);
        mv.invoke(codegen.methodDesc(node, ScriptName.Code));
        mv._return();
    }

    /**
     * Emit: {@code realm = context.getRealm()}
     */
    private void getRealm(Variable<ExecutionContext> context, Variable<Realm> realm,
            InstructionVisitor mv) {
        mv.load(context);
        mv.invoke(Methods.ExecutionContext_getRealm);
        mv.store(realm);
    }

    /**
     * Emit: {@code context = ExecutionContext.newScriptExecutionContext(realm, script)}
     */
    private void newScriptExecutionContext(Variable<Realm> realm,
            Variable<com.github.anba.es6draft.Script> script, Variable<ExecutionContext> context,
            InstructionVisitor mv) {
        mv.load(realm);
        mv.load(script);
        mv.invoke(Methods.ExecutionContext_newScriptExecutionContext);
        mv.store(context);
    }

    /**
     * Emit:
     * {@code context = ExecutionContext.newEvalExecutionContext(callerContext, script, varEnv, lexEnv)}
     */
    private void newEvalExecutionContext(Variable<ExecutionContext> callerContext,
            Variable<com.github.anba.es6draft.Script> script,
            Variable<? extends LexicalEnvironment<?>> varEnv,
            Variable<? extends LexicalEnvironment<?>> lexEnv, Variable<ExecutionContext> context,
            InstructionVisitor mv) {
        mv.load(callerContext);
        mv.load(script);
        mv.load(varEnv);
        mv.load(lexEnv);
        mv.invoke(Methods.ExecutionContext_newEvalExecutionContext);
        mv.store(context);
    }

    /**
     * Emit: {@code env = cx.getRealm().getGlobalEnv()}
     */
    private void getGlobalEnv(Variable<ExecutionContext> context,
            Variable<? extends LexicalEnvironment<?>> env, InstructionVisitor mv) {
        mv.load(context);
        mv.invoke(Methods.ExecutionContext_getRealm);
        mv.invoke(Methods.Realm_getGlobalEnv);
        mv.store(env);
    }

    /**
     * Emit: {@code realm = realm.getScriptContext()}
     */
    private void getScriptContext(Variable<Realm> realm, Variable<ExecutionContext> context,
            InstructionVisitor mv) {
        mv.load(realm);
        mv.invoke(Methods.Realm_getScriptContext);
        mv.store(context);
    }

    /**
     * Emit: {@code realm.setScriptContext(context)}
     */
    private void setScriptContext(Variable<Realm> realm, Variable<ExecutionContext> context,
            InstructionVisitor mv) {
        mv.load(realm);
        mv.load(context);
        mv.invoke(Methods.Realm_setScriptContext);
    }

    /**
     * Emit: {@code env = LexicalEnvironment.newDeclarativeEnvironment(outer)}
     */
    private void newDeclarativeEnvironment(Value<? extends LexicalEnvironment<?>> outer,
            Variable<? extends LexicalEnvironment<?>> env, InstructionVisitor mv) {
        mv.load(outer);
        mv.invoke(Methods.LexicalEnvironment_newDeclarativeEnvironment);
        mv.store(env);
    }

    /**
     * Emit:
     * {@code env = LexicalEnvironment.newDeclarativeEnvironment(context.getLexicalEnvironment())}
     */
    private void newDeclarativeEnvironment(Variable<ExecutionContext> context,
            Variable<? extends LexicalEnvironment<?>> env, InstructionVisitor mv) {
        mv.load(context);
        mv.invoke(Methods.ExecutionContext_getLexicalEnvironment);
        mv.invoke(Methods.LexicalEnvironment_newDeclarativeEnvironment);
        mv.store(env);
    }

    /**
     * Emit: {@code env = context.getVariableEnvironment()}
     */
    private void getVariableEnvironment(Variable<ExecutionContext> context,
            Variable<? extends LexicalEnvironment<?>> env, InstructionVisitor mv) {
        mv.load(context);
        mv.invoke(Methods.ExecutionContext_getVariableEnvironment);
        mv.store(env);
    }

    /**
     * Emit: {@code env = context.getLexicalEnvironment()}
     */
    private void getLexicalEnvironment(Variable<ExecutionContext> context,
            Variable<? extends LexicalEnvironment<?>> env, InstructionVisitor mv) {
        mv.load(context);
        mv.invoke(Methods.ExecutionContext_getLexicalEnvironment);
        mv.store(env);
    }

    /**
     * Emit: {@code context.setLexicalEnvironment(env)}
     */
    private void setLexicalEnvironment(Variable<ExecutionContext> context,
            Variable<LexicalEnvironment<DeclarativeEnvironmentRecord>> env, InstructionVisitor mv) {
        mv.load(context);
        mv.load(env);
        mv.invoke(Methods.ExecutionContext_setLexicalEnvironment);
    }
}
