package org.jruby.ir; 

import org.jruby.ast.ArgsNode;
import org.jruby.ast.ArgsPushNode;
import org.jruby.ast.ArgumentNode;
import org.jruby.ast.ArrayNode;
import org.jruby.ast.BlockArgNode;
import org.jruby.ast.DAsgnNode;
import org.jruby.ast.EncodingNode;
import org.jruby.ast.IterNode;
import org.jruby.ast.LambdaNode;
import org.jruby.ast.ListNode;
import org.jruby.ast.LocalAsgnNode;
import org.jruby.ast.MultipleAsgn19Node;
import org.jruby.ast.Node;
import org.jruby.ast.NotNode;
import org.jruby.ast.NthRefNode;
import org.jruby.ast.OptArgNode;
import org.jruby.ast.StarNode;
import org.jruby.ast.YieldNode;
import org.jruby.compiler.NotCompilableException;
import org.jruby.ir.instructions.BEQInstr;
import org.jruby.ir.instructions.BNEInstr;
import org.jruby.ir.instructions.CallInstr;
import org.jruby.ir.instructions.CheckArityInstr;
import org.jruby.ir.instructions.ClosureReturnInstr;
import org.jruby.ir.instructions.CopyInstr;
import org.jruby.ir.instructions.LabelInstr;
import org.jruby.ir.instructions.ReceiveClosureInstr;
import org.jruby.ir.instructions.ReceivePreReqdArgInstr;
import org.jruby.ir.instructions.ReceiveSelfInstr;
import org.jruby.ir.instructions.ReqdArgMultipleAsgnInstr;
import org.jruby.ir.instructions.RestArgMultipleAsgnInstr;
import org.jruby.ir.instructions.ToAryInstr;
import org.jruby.ir.instructions.YieldInstr;
import org.jruby.ir.instructions.defined.BackrefIsMatchDataInstr;
import org.jruby.ir.instructions.ruby19.BuildLambdaInstr;
import org.jruby.ir.instructions.ruby19.GetEncodingInstr;
import org.jruby.ir.instructions.ruby19.ReceiveOptArgInstr19;
import org.jruby.ir.instructions.ruby19.ReceivePostReqdArgInstr;
import org.jruby.ir.instructions.ruby19.ReceiveRestArgInstr19;
import org.jruby.ir.operands.CompoundArray;
import org.jruby.ir.operands.Label;
import org.jruby.ir.operands.LocalVariable;
import org.jruby.ir.operands.MethAddr;
import org.jruby.ir.operands.NthRef;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.StringLiteral;
import org.jruby.ir.operands.UndefinedValue;
import org.jruby.ir.operands.Variable;
import org.jruby.javasupport.util.RuntimeHelpers;
import org.jruby.runtime.Arity;

public class IRBuilder19 extends IRBuilder {
    public IRBuilder19(IRManager manager) {
        super(manager);
    }
    
    @Override
    public boolean is1_9() {
        return true;
    }    

    @Override
    protected Operand buildVersionSpecificNodes(Node node, IRScope s) {
        switch (node.getNodeType()) {
            case ENCODINGNODE: return buildEncoding((EncodingNode)node, s);
            case MULTIPLEASGN19NODE: return buildMultipleAsgn19((MultipleAsgn19Node) node, s);
            case LAMBDANODE: return buildLambda((LambdaNode)node, s);
            default: throw new NotCompilableException("Unknown node encountered in builder: " + node.getClass());
        }
    }

    @Override
    protected LocalVariable getBlockArgVariable(IRScope s, String name, int depth) {
        IRClosure cl = (IRClosure)s;
        if (cl.isForLoopBody()) {
            return cl.getLocalVariable(name, depth);
        } else {
            throw new NotCompilableException("Cannot ask for block-arg variable in 1.9 mode");
        }
    }

    @Override
    public void buildVersionSpecificBlockArgsAssignment(Node node, IRScope s, Operand argsArray, int argIndex, boolean isMasgnRoot, boolean isClosureArg, boolean isSplat) {
        IRClosure cl = (IRClosure)s;
        if (!cl.isForLoopBody())
            throw new NotCompilableException("Should not have come here for block args assignment in 1.9 mode: " + node);

        // Argh!  For-loop bodies and regular iterators are different in terms of block-args!
        switch (node.getNodeType()) {
            case MULTIPLEASGN19NODE: {
                ListNode sourceArray = ((MultipleAsgn19Node) node).getPre();
                int i = 0; 
                for (Node an: sourceArray.childNodes()) {
                    // Use 1.8 mode version for this
                    buildBlockArgsAssignment(an, s, null, i, false, false, false);
                    i++;
                }
                break;
            }
            default: 
                throw new NotCompilableException("Can't build assignment node: " + node);
        }
    }

    protected LocalVariable getArgVariable(IRScope s, String name, int depth) {
        // For non-loops, this name will override any name that exists in outer scopes
        return s.isForLoopBody() ? s.getLocalVariable(name, depth) : s.getNewLocalVariable(name, 0);
    }

    private void addArgReceiveInstr(IRScope s, Variable v, int argIndex, boolean post, int numPreReqd, int numPostRead) {
        if (post) s.addInstr(new ReceivePostReqdArgInstr(v, argIndex, numPreReqd, numPostRead));
        else s.addInstr(new ReceivePreReqdArgInstr(v, argIndex));
    }

    public void receiveRequiredArg(Node node, IRScope s, int argIndex, boolean post, int numPreReqd, int numPostRead) {
        switch (node.getNodeType()) {
            case ARGUMENTNODE: {
                ArgumentNode a = (ArgumentNode)node;
                String argName = a.getName();
                if (s instanceof IRMethod) ((IRMethod)s).addArgDesc("req", argName);
                addArgReceiveInstr(s, s.getNewLocalVariable(argName, 0), argIndex, post, numPreReqd, numPostRead);
                break;
            }
            case MULTIPLEASGN19NODE: {
                MultipleAsgn19Node childNode = (MultipleAsgn19Node) node;
                Variable v = s.getNewTemporaryVariable();
                addArgReceiveInstr(s, v, argIndex, post, numPreReqd, numPostRead);
                if (s instanceof IRMethod) ((IRMethod)s).addArgDesc("rest", "");
                s.addInstr(new ToAryInstr(v, v, manager.getFalse()));
                buildMultipleAsgn19Assignment(childNode, s, v, null);
                break;
            }
            default: throw new NotCompilableException("Can't build assignment node: " + node);
        }
    }

    private void receiveClosureArg(BlockArgNode blockVarNode, IRScope s) {
        Variable blockVar = null;
        if (blockVarNode != null) {
            String blockArgName = blockVarNode.getName();
            blockVar = s.getNewLocalVariable(blockArgName, 0);
            if (s instanceof IRMethod) ((IRMethod)s).addArgDesc("block", blockArgName);
            s.addInstr(new ReceiveClosureInstr(blockVar));
        }

        // SSS FIXME: This instruction is only needed if there is an yield instr somewhere!
        // In addition, store the block argument in an implicit block variable
        Variable implicitBlockArg = s.getImplicitBlockArg();
        if (blockVar == null) s.addInstr(new ReceiveClosureInstr(implicitBlockArg));
        else s.addInstr(new CopyInstr(implicitBlockArg, blockVar));
    }

    public void receiveArgs(final ArgsNode argsNode, IRScope s) {
        final int numPreReqd = argsNode.getPreCount();
        final int numPostReqd = argsNode.getPostCount();
        final int required = argsNode.getRequiredArgsCount(); // numPreReqd + numPostReqd
        int opt = argsNode.getOptionalArgsCount();
        int rest = argsNode.getRestArg();

        s.getStaticScope().setArities(required, opt, rest);

        // For closures, we don't need the check arity call
        if (s instanceof IRMethod) {
            // FIXME: Expensive to do this explicitly?  But, two advantages:
            // (a) on inlining, we'll be able to get rid of these checks in almost every case.
            // (b) compiler to bytecode will anyway generate this and this is explicit.
            // For now, we are going explicit instruction route.  But later, perhaps can make this implicit in the method setup preamble?  
            s.addInstr(new CheckArityInstr(required, opt, rest));
        }

        // Other args begin at index 0
        int argIndex = 0;

        // Pre(-opt and rest) required args
        ListNode preArgs = argsNode.getPre();
        for (int i = 0; i < numPreReqd; i++, argIndex++) {
            receiveRequiredArg(preArgs.get(i), s, argIndex, false, -1, -1);
        }

        // Fixup opt/rest
        opt = opt > 0 ? opt : 0;
        rest = rest > -1 ? 1 : 0;

        // Now for opt args
        if (opt > 0) {
            ListNode optArgs = argsNode.getOptArgs();
            for (int j = 0; j < opt; j++, argIndex++) {
                // Jump to 'l' if this arg is not null.  If null, fall through and build the default value!
                Label l = s.getNewLabel();
                OptArgNode n = (OptArgNode)optArgs.get(j);
                String argName = n.getName();
                Variable av = s.getNewLocalVariable(argName, 0);
                if (s instanceof IRMethod) ((IRMethod)s).addArgDesc("opt", argName);
                // You need at least required+j+1 incoming args for this opt arg to get an arg at all
                s.addInstr(new ReceiveOptArgInstr19(av, argIndex, required+j+1));
                s.addInstr(BNEInstr.create(av, UndefinedValue.UNDEFINED, l)); // if 'av' is not undefined, go to default
                build(n.getValue(), s);
                s.addInstr(new LabelInstr(l));
            }
        }

        // Rest arg
        if (rest > 0) {
            // Consider: def foo(*); .. ; end
            // For this code, there is no argument name available from the ruby code.
            // So, we generate an implicit arg name
            String argName = argsNode.getRestArgNode().getName();
            if (s instanceof IRMethod) ((IRMethod)s).addArgDesc("rest", argName == null ? "" : argName);
            argName = (argName == null || argName.equals("")) ? "%_arg_array" : argName;

            // You need at least required+opt+1 incoming args for the rest arg to get any args at all
            // If it is going to get something, then it should ignore required+opt args from the beginning
            // because they have been accounted for already.
            s.addInstr(new ReceiveRestArgInstr19(s.getNewLocalVariable(argName, 0), argIndex, required, opt));
            argIndex++;
        }

        // Post(-opt and rest) required args
        ListNode postArgs = argsNode.getPost();
        for (int i = 0; i < numPostReqd; i++) {
            receiveRequiredArg(postArgs.get(i), s, i, true, numPreReqd, numPostReqd);
        }

        // Now, receive the block arg 
        // -- for methods, we always receive it (implicitly, if the block arg is not explicit)
        // -- for closures, only if it is explicitly present
        BlockArgNode blockArg = argsNode.getBlock();
        if ((s instanceof IRMethod) || (blockArg != null)) receiveClosureArg(blockArg, s);
    }

    @Override
    public void receiveBlockArgs(final IterNode node, IRScope s) {
        Node args = node.getVarNode();
        if (args instanceof ArgsNode) { // regular blocks
            ((IRClosure)s).setParameterList(RuntimeHelpers.encodeParameterList((ArgsNode)args).split(";"));
            receiveArgs((ArgsNode)args, s);
        } else  { 
            // for loops -- reuse code in IRBuilder:buildBlockArgsAssignment
            buildBlockArgsAssignment(args, s, null, 0, false, false, false);
        }
    }

    @Override
    public void receiveBlockClosureArg(Node node, IRScope s) {
        // Nothing to do here.  iterNode.blockVarNode is not valid in 1.9 mode
    }

    @Override
    public void receiveMethodArgs(final ArgsNode argsNode, IRScope s) {
        receiveArgs(argsNode, s);
    }

    protected void receiveArg(IRScope s, Variable v, Operand argsArray, int argIndex, boolean isSplat) {
        // We are in a nested receive situation -- when we are not at the root of a masgn tree
        // Ex: We are trying to receive (b,c) in this example: "|a, (b,c), d| = ..."
    }

    // This method is called to build arguments
    public void buildArgsMasgn(Node node, IRScope s, Operand argsArray, boolean isMasgnRoot, int preArgsCount, int postArgsCount, int index, boolean isSplat) {
        Variable v;
        switch (node.getNodeType()) {
            case DASGNNODE: {
                DAsgnNode dynamicAsgn = (DAsgnNode) node;
                v = getArgVariable(s, dynamicAsgn.getName(), dynamicAsgn.getDepth());
                if (isSplat) s.addInstr(new RestArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                else s.addInstr(new ReqdArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                break;
            }
            case LOCALASGNNODE: {
                LocalAsgnNode localVariable = (LocalAsgnNode) node;
                v = getArgVariable(s, localVariable.getName(), localVariable.getDepth());
                if (isSplat) s.addInstr(new RestArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                else s.addInstr(new ReqdArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                break;
            }
            case MULTIPLEASGN19NODE: {
                Variable oldArgs = null;
                MultipleAsgn19Node childNode = (MultipleAsgn19Node) node;
                if (!isMasgnRoot) {
                    v = s.getNewTemporaryVariable();
                    if (isSplat) s.addInstr(new RestArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                    else s.addInstr(new ReqdArgMultipleAsgnInstr(v, argsArray, preArgsCount, postArgsCount, index));
                    s.addInstr(new ToAryInstr(v, v, manager.getFalse()));
                    argsArray = v;
                }
                // Build
                buildMultipleAsgn19Assignment(childNode, s, argsArray, null);
                break;
            }
            default:
                throw new NotCompilableException("Shouldn't get here: " + node);
        }
    }

    // SSS: This method is called both for regular multiple assignment as well as argument passing
    //
    // Ex: a,b,*c=v  is a regular assignment and in this case, the "values" operand will be non-null
    // Ex: { |a,b,*c| ..} is the argument passing case
    public void buildMultipleAsgn19Assignment(final MultipleAsgn19Node multipleAsgnNode, IRScope s, Operand argsArray, Operand values) {
        final ListNode masgnPre = multipleAsgnNode.getPre();

        // Build assignments for specific named arguments
        int i = 0; 
        if (masgnPre != null) {
            for (Node an: masgnPre.childNodes()) {
                if (values == null) {
                    buildArgsMasgn(an, s, argsArray, false, -1, -1, i, false);
                } else {
                    Variable rhsVal = s.getNewTemporaryVariable();
                    s.addInstr(new ReqdArgMultipleAsgnInstr(rhsVal, values, i));
                    buildAssignment(an, s, rhsVal);
                }
                i++;
            }
        }

        // Build an assignment for a splat, if any, with the rest of the operands!
        Node restNode = multipleAsgnNode.getRest();
        int postArgsCount = multipleAsgnNode.getPostCount();
        if (restNode != null) {
            if (restNode instanceof StarNode) {
                // do nothing
            } else if (values == null) {
                buildArgsMasgn(restNode, s, argsArray, false, i, postArgsCount, 0, true); // rest of the argument array!
            } else {
                Variable rhsVal = s.getNewTemporaryVariable();
                s.addInstr(new RestArgMultipleAsgnInstr(rhsVal, values, i, postArgsCount, 0));
                buildAssignment(restNode, s, rhsVal); // rest of the argument array!
            }
        }

        // Build assignments for rest of the operands
        final ListNode masgnPost = multipleAsgnNode.getPost();
        if (masgnPost != null) {
            int j = 0;
            for (Node an: masgnPost.childNodes()) {
                if (values == null) {
                    buildArgsMasgn(an, s, argsArray, false, i, postArgsCount, j, false);
                } else {
                    Variable rhsVal = s.getNewTemporaryVariable();
                    s.addInstr(new ReqdArgMultipleAsgnInstr(rhsVal, values, i, postArgsCount, j));  // Fetch from the end
                    buildAssignment(an, s, rhsVal);
                }
                j++;
            }
        }
    }

    // Non-arg masgn (actually a nested masgn)
    @Override
    public void buildVersionSpecificAssignment(Node node, IRScope s, Variable v) {
        switch (node.getNodeType()) {
        case MULTIPLEASGN19NODE: {
            s.addInstr(new ToAryInstr(v, v, manager.getFalse()));
            buildMultipleAsgn19Assignment((MultipleAsgn19Node)node, s, null, v);
            break;
        }
        default: 
            throw new NotCompilableException("Can't build assignment node: " + node);
        }
    }

    @Override
    public Operand buildArgsPush(final ArgsPushNode node, IRScope s) {
        Operand v1 = build(node.getFirstNode(), s);
        Operand v2 = build(node.getSecondNode(), s);
        return new CompoundArray(v1, v2, true);
    }

    public Operand buildEncoding(EncodingNode node, IRScope s) {
        Variable ret = s.getNewTemporaryVariable();
        s.addInstr(new GetEncodingInstr(ret, node.getEncoding()));
        return ret;
    }

    public Operand buildLambda(LambdaNode node, IRScope s) {
        IRClosure closure = new IRClosure(manager, s, false, node.getPosition().getStartLine(), node.getScope(), Arity.procArityOf(node.getArgs()), node.getArgumentType(), true);
        s.addClosure(closure);

        // Create a new nested builder to ensure this gets its own IR builder state 
        // like the ensure block stack
        IRBuilder closureBuilder = createIRBuilder(manager, is1_9());

        // Receive self
        closure.addInstr(new ReceiveSelfInstr(getSelf(closure)));

        // args
        closureBuilder.receiveBlockArgs(node, closure);
        closureBuilder.receiveBlockClosureArg(node.getBlockVarNode(), closure);

        Operand closureRetVal = node.getBody() == null ? manager.getNil() : closureBuilder.build(node.getBody(), closure);

        // can be U_NIL if the node is an if node with returns in both branches.
        if (closureRetVal != U_NIL) closure.addInstr(new ClosureReturnInstr(closureRetVal));

        Variable lambda = s.getNewTemporaryVariable();
        s.addInstr(new BuildLambdaInstr(lambda, closure, node.getPosition()));
        return lambda;
    }

    @Override
    public Operand buildYield(YieldNode node, IRScope s) {
        boolean unwrap = true;
        Node argNode = node.getArgsNode();
        // Get rid of one level of array wrapping
        if (argNode != null && (argNode instanceof ArrayNode) && ((ArrayNode)argNode).size() == 1) {
            argNode = ((ArrayNode)argNode).getLast();
            unwrap = false;
        }

        Variable ret = s.getNewTemporaryVariable();
        s.addInstr(new YieldInstr(ret, s.getImplicitBlockArg(), build(argNode, s), unwrap));
        return ret;
    }

    // Non-arg masgn
    public Operand buildMultipleAsgn19(MultipleAsgn19Node multipleAsgnNode, IRScope s) {
        Operand  values = build(multipleAsgnNode.getValueNode(), s);
        Variable ret = getValueInTemporaryVariable(s, values);
        s.addInstr(new ToAryInstr(ret, ret, manager.getFalse()));
        buildMultipleAsgn19Assignment(multipleAsgnNode, s, null, ret);
        return ret;
    }

    // 1.9 specific defined? logic  
    @Override
    public Operand buildVersionSpecificGetDefinitionIR(Node node, IRScope s) {
        switch (node.getNodeType()) {
            case ORNODE:
            case ANDNODE: {
                return new StringLiteral("expression");
            }
            case MULTIPLEASGN19NODE: {
                return new StringLiteral("assignment");
            }
            case DVARNODE: {
                return new StringLiteral("local-variable");
            }
            case BACKREFNODE: {
                return buildDefinitionCheck(s, new BackrefIsMatchDataInstr(s.getNewTemporaryVariable()), "global-variable");
            }
            case DREGEXPNODE:
            case DSTRNODE: {
                Operand v = super.buildVersionSpecificGetDefinitionIR(node, s);
                Label doneLabel = s.getNewLabel();
                Variable tmpVar = getValueInTemporaryVariable(s, v);
                s.addInstr(BNEInstr.create(tmpVar, manager.getNil(), doneLabel));
                s.addInstr(new CopyInstr(tmpVar, new StringLiteral("expression")));
                s.addInstr(new LabelInstr(doneLabel));
                return tmpVar;
            }
            case NOTNODE: {
                Operand v = buildGetDefinitionBase(((NotNode)node).getConditionNode(), s);
                Label doneLabel = s.getNewLabel();
                Variable tmpVar = getValueInTemporaryVariable(s, v);
                s.addInstr(BEQInstr.create(tmpVar, manager.getNil(), doneLabel));
                s.addInstr(new CopyInstr(tmpVar, new StringLiteral("method")));
                s.addInstr(new LabelInstr(doneLabel));
                return tmpVar;
            }
            case NTHREFNODE: {
            // SSS FIXME: Is there a reason to do this all with low-level IR?
            // Can't this all be folded into a Java method that would be part
            // of the runtime library, which then can be used by buildDefinitionCheck method above?
            // This runtime library would be used both by the interpreter & the compiled code!

                /* -------------------------------------------------------------------------------------
                 * We have to generate IR for this:
                 *    v = backref; (!(v instanceof RubyMatchData) || v.group(n).nil?) ? nil : "global-variable"
                 *
                 * which happens to be identical to: (where nthRef implicitly fetches backref again!)
                 *    v = backref; (!(v instanceof RubyMatchData) || nthRef(n).nil?) ? nil : "global-variable"
                 *
                 * I am using the second form since it let us encode it in fewer IR instructions.
                 * But, note that this second form is not as clean as the first one plus it fetches backref twice!
                 * ------------------------------------------------------------------------------------- */
                int n = ((NthRefNode) node).getMatchNumber();
                Label undefLabel = s.getNewLabel();
                Variable tmpVar = s.getNewTemporaryVariable();
                s.addInstr(new BackrefIsMatchDataInstr(tmpVar));
                s.addInstr(BEQInstr.create(tmpVar, manager.getFalse(), undefLabel));
                // SSS FIXME: 
                // - Can/should I use BEQInstr(new NthRef(n), manager.getNil(), undefLabel)? instead of .nil? & compare with flag?
                // - Or, even create a new IsNilInstr and NotNilInstr to represent optimized scenarios where
                //   the nil? method is not monkey-patched?
                // This matters because if String.nil? is monkey-patched, the two sequences can behave differently.
                s.addInstr(CallInstr.create(tmpVar, new MethAddr("nil?"), new NthRef(n), NO_ARGS, null));
                s.addInstr(BEQInstr.create(tmpVar, manager.getTrue(), undefLabel));
                return buildDefnCheckIfThenPaths(s, undefLabel, new StringLiteral("global-variable"));
            }
            default: {
                return buildGenericGetDefinitionIR(node, s);
            }
        }
    }
}
