package com.cloudbees.groovy.cps;

import com.cloudbees.groovy.cps.impl.ArrayAccessBlock;
import com.cloudbees.groovy.cps.impl.AssertBlock;
import com.cloudbees.groovy.cps.impl.AssignmentBlock;
import com.cloudbees.groovy.cps.impl.AttributeAccessBlock;
import com.cloudbees.groovy.cps.impl.BlockScopedBlock;
import com.cloudbees.groovy.cps.impl.BreakBlock;
import com.cloudbees.groovy.cps.impl.CallSiteBlock;
import com.cloudbees.groovy.cps.impl.ClosureBlock;
import com.cloudbees.groovy.cps.impl.ConstantBlock;
import com.cloudbees.groovy.cps.impl.ContinueBlock;
import com.cloudbees.groovy.cps.impl.CpsClosure;
import com.cloudbees.groovy.cps.impl.DoWhileBlock;
import com.cloudbees.groovy.cps.impl.ElvisBlock;
import com.cloudbees.groovy.cps.impl.ExcrementOperatorBlock;
import com.cloudbees.groovy.cps.impl.ForInLoopBlock;
import com.cloudbees.groovy.cps.impl.ForLoopBlock;
import com.cloudbees.groovy.cps.impl.FunctionCallBlock;
import com.cloudbees.groovy.cps.impl.IfBlock;
import com.cloudbees.groovy.cps.impl.JavaThisBlock;
import com.cloudbees.groovy.cps.impl.ListBlock;
import com.cloudbees.groovy.cps.impl.LocalVariableBlock;
import com.cloudbees.groovy.cps.impl.LogicalOpBlock;
import com.cloudbees.groovy.cps.impl.MapBlock;
import com.cloudbees.groovy.cps.impl.MethodPointerBlock;
import com.cloudbees.groovy.cps.impl.NewArrayBlock;
import com.cloudbees.groovy.cps.impl.NewArrayFromInitializersBlock;
import com.cloudbees.groovy.cps.impl.NotBlock;
import com.cloudbees.groovy.cps.impl.PropertyAccessBlock;
import com.cloudbees.groovy.cps.impl.ReturnBlock;
import com.cloudbees.groovy.cps.impl.SequenceBlock;
import com.cloudbees.groovy.cps.impl.SourceLocation;
import com.cloudbees.groovy.cps.impl.StaticFieldBlock;
import com.cloudbees.groovy.cps.impl.SuperBlock;
import com.cloudbees.groovy.cps.impl.SwitchBlock;
import com.cloudbees.groovy.cps.impl.ThrowBlock;
import com.cloudbees.groovy.cps.impl.TryCatchBlock;
import com.cloudbees.groovy.cps.impl.VariableDeclBlock;
import com.cloudbees.groovy.cps.impl.WhileBlock;
import com.cloudbees.groovy.cps.impl.YieldBlock;
import com.cloudbees.groovy.cps.sandbox.CallSiteTag;
import com.cloudbees.groovy.cps.sandbox.Invoker;
import groovy.lang.Closure;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;

import java.util.*;

import static com.cloudbees.groovy.cps.Block.*;
import static java.util.Arrays.*;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.kohsuke.groovy.sandbox.impl.Checker;

/**
 * Builder pattern for constructing {@link Block}s into a tree.
 *
 * For example, to build a {@link Block} that represents "1+1", you'd call {@code plus(one(),one())}
 *
 * @author Kohsuke Kawaguchi
 */
public class Builder {
    private final MethodLocation loc;
    private Class<? extends CpsClosure> closureType;
    private final Collection<CallSiteTag> tags;

    public Builder(MethodLocation loc) {
        this.loc = loc;
        this.tags = Collections.emptySet();
    }

    private Builder(Builder parent, Collection<CallSiteTag> newTags) {
        this.loc = parent.loc;
        this.closureType = parent.closureType;
        this.tags = combine(parent.tags,newTags);
    }

    private Collection<CallSiteTag> combine(Collection<CallSiteTag> a, Collection<CallSiteTag> b) {
        if (a.isEmpty())    return b;
        if (b.isEmpty())    return a;
        Collection<CallSiteTag> all = new ArrayList<CallSiteTag>(a);
        all.addAll(b);
        return all;
    }

    /**
     * Overrides the actual instance type of {@link CpsClosure} to be created.
     *
     * @return 'this' object for the fluent API pattern.
     */
    public Builder withClosureType(Class<? extends CpsClosure> t) {
        closureType = t;
        return this;
    }

    /**
     * Returns a new {@link Builder} that contextualizes call sites with the given tags.
     *
     * @see Invoker#contextualize(CallSiteBlock)
     */
    public Builder contextualize(CallSiteTag... tags) {
        return new Builder(this,Arrays.asList(tags));
    }

    /**
     * Evaluate the given closure by passing this object as an argument.
     * Used to bind literal Builder to a local variable.
     */
    public Object with(Closure c) {
        return c.call(this);
    }

    private static final Block NULL = new ConstantBlock(null);
    private static final LValueBlock THIS = new LocalVariableBlock(null, "this");
    private static final Block JAVA_THIS = new JavaThisBlock();

    public Block null_() {
        return NULL;
    }

    public Block noop() {
        return NOOP;
    }

    public Block constant(Object o) {
        return new ConstantBlock(o);
    }

    public Block methodPointer(int line, Block lhs, Block methodName) {
        return new MethodPointerBlock(loc(line),lhs,methodName, tags);
    }

    public Block zero() {
        return constant(0);
    }

    public Block one() {
        return constant(1);
    }

    public Block two() {
        return constant(2);
    }

    public Block true_() {
        return constant(true);
    }

    public Block false_() {
        return constant(false);
    }

    /**
     * {@code { ... }}
     */
    public Block block(Block... bodies) {
        if (bodies.length==0)   return NULL;

        Block e = bodies[0];
        for (int i=1; i<bodies.length; i++)
            e = sequence(e,bodies[i]);

        return blockScoped(e);
    }

    /**
     * Creates a block scope of variables around the given expression
     */
    private Block blockScoped(final Block exp) {
        return new BlockScopedBlock(exp);
    }

    /**
     * Like {@link #block(Block...)} but it doesn't create a new scope.
     *
     */
    public Block sequence(Block... bodies) {
        if (bodies.length==0)   return NULL;

        Block e = bodies[0];
        for (int i=1; i<bodies.length; i++)
            e = sequence(e,bodies[i]);

        return e;
    }

    public Block sequence(final Block exp1, final Block exp2) {
        return new SequenceBlock(exp1, exp2);
    }

    public Block sequence(Block b) {
        return b;
    }

    public Block closure(int line, List<Class> parameterTypes, List<String> parameters, Block body) {
        return new ClosureBlock(loc(line),parameterTypes,parameters,body,closureType);
    }

    public LValueBlock localVariable(String name) {
        return new LocalVariableBlock(null, name);
    }

    public LValueBlock localVariable(int line, String name) {
        return new LocalVariableBlock(loc(line), name);
    }

    public Block setLocalVariable(int line, final String name, final Block rhs) {
        return assign(line,localVariable(line, name),rhs);
    }

    public Block declareVariable(final Class type, final String name) {
        return new VariableDeclBlock(type, name);
    }

    public Block declareVariable(int line, Class type, String name, Block init) {
        return sequence(
            declareVariable(type,name),
            setLocalVariable(line,name, init));
    }

    public Block this_() {
        return THIS; // this is 'groovyThis'
    }

    /**
     * Block that's only valid as a LHS of a method call like {@code super.foo(...)}
     */
    public Block super_(Class senderType) {
        return new SuperBlock(senderType);
    }

    /**
     * See {@link JavaThisBlock} for the discussion of {@code this} vs {@code javaThis}
     */
    public Block javaThis_() {
        return JAVA_THIS;
    }

    /**
     * Assignment operator to a local variable, such as {@code x += 3}
     */
    public Block localVariableAssignOp(int line, String name, String operator, Block rhs) {
        return setLocalVariable(line, name, functionCall(line, localVariable(line, name), operator, rhs));
    }

    /**
     * {@code if (...) { ... } else { ... }}
     */
    public Block if_(Block cond, Block then, Block els) {
        return new IfBlock(cond,then,els);
    }

    public Block if_(Block cond, Block then) {
        return if_(cond, then, NOOP);
    }

    /**
     * {@code for (e1; e2; e3) { ... }}
     */
    public Block forLoop(String label, Block e1, Block e2, Block e3, Block body) {
        return new ForLoopBlock(label, e1,e2,e3,body);
    }

    /**
     * {@code for (x in col) { ... }}
     */
    public Block forInLoop(int line, String label, Class type, String variable, Block collection, Block body) {
        return new ForInLoopBlock(loc(line), label,type,variable,collection,body);
    }

    public Block break_(String label) {
        if (label==null)    return BreakBlock.INSTANCE;
        return new BreakBlock(label);
    }

    public Block continue_(String label) {
        if (label==null)    return ContinueBlock.INSTANCE;
        return new ContinueBlock(label);
    }

    public Block while_(String label, Block cond, Block body) {
        return new WhileBlock(label,cond,body);
    }

    public Block doWhile(String label, Block body, Block cond) {
        return new DoWhileBlock(label,body,cond);
    }

    public Block tryCatch(Block body, Block finally_, CatchExpression... catches) {
        return tryCatch(body, asList(catches), finally_);
    }


    /**
     * <pre>{@code
     * try {
     *     ...
     * } catch (T v) {
     *     ...
     * } catch (T v) {
     *     ...
     * }
     * }</pre>
     */
    public Block tryCatch(final Block body, final List<CatchExpression> catches) {
        return tryCatch(body, catches, null);
    }

    public Block tryCatch(final Block body, final List<CatchExpression> catches, final Block finally_) {
        return new TryCatchBlock(catches, body, finally_);
    }

    /**
     * {@code throw exp;}
     */
    public Block throw_(int line, final Block exp) {
        return new ThrowBlock(loc(line),exp,false);
    }

    /**
     * Map literal: {@code [ a:b, c:d, e:f ] ...}
     *
     * We expect arguments to be multiple of two.
     */
    public Block map(Block... blocks) {
        return new MapBlock(blocks);
    }

    public Block map(List<Block> blocks) {
        return map(blocks.toArray(new Block[blocks.size()]));
    }

    public Block staticCall(int line, Class lhs, String name, Block... argExps) {
        return functionCall(line, constant(lhs), name, argExps);
    }

    public Block plus(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "plus", rhs);
    }

    public Block plusEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "plus");
    }

    public Block minus(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "minus", rhs);
    }

    public Block minusEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "minus");
    }

    public Block multiply(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"multiply",rhs);
    }

    public Block multiplyEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "multiply");
    }

    public Block div(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"div",rhs);
    }

    public Block divEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "div");
    }

    public Block intdiv(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"intdiv",rhs);
    }

    public Block intdivEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "intdiv");
    }

    public Block mod(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "mod", rhs);
    }

    public Block modEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "mod");
    }

    public Block power(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs, "power", rhs);
    }

    public Block powerEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "power");
    }

    public Block unaryMinus(int line, Block lhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "unaryMinus", lhs);
    }

    public Block unaryPlus(int line, Block lhs) {
        return staticCall(line,ScriptBytecodeAdapter.class, "unaryPlus", lhs);
    }

    public Block ternaryOp(Block cond, Block trueExp, Block falseExp) {
        return if_(cond, trueExp, falseExp);
    }

    /**
     * {@code x ?: y}
     */
    public Block elvisOp(Block cond, Block falseExp) {
        return new ElvisBlock(cond,falseExp);
    }

    public Block compareEqual(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "compareEqual", lhs, rhs);
    }

    public Block compareNotEqual(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "compareNotEqual", lhs, rhs);
    }

    public Block compareTo(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "compareTo", lhs, rhs);
    }

    public Block lessThan(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "compareLessThan", lhs, rhs);
    }

    public Block lessThanEqual(int line, Block lhs, Block rhs) {
        return staticCall(line,ScriptBytecodeAdapter.class,"compareLessThanEqual",lhs,rhs);
    }

    public Block greaterThan(int line, Block lhs, Block rhs) {
        return staticCall(line,ScriptBytecodeAdapter.class,"compareGreaterThan",lhs,rhs);
    }

    public Block greaterThanEqual(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "compareGreaterThanEqual", lhs, rhs);
    }

    /**
     * {@code lhs =~ rhs}
     */
    public Block findRegex(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "findRegex", lhs, rhs);
    }

    /**
     * {@code lhs ==~ rhs}
     */
    public Block matchRegex(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "matchRegex", lhs, rhs);
    }

    /**
     * {@code lhs in rhs}
     */
    public Block isCase(int line, Block lhs, Block rhs) {
        return staticCall(line, ScriptBytecodeAdapter.class, "isCase", lhs, rhs);
    }

    /**
     * {@code lhs && rhs}
     */
    public Block logicalAnd(int line, Block lhs, Block rhs) {
        return new LogicalOpBlock(lhs,rhs,true);
    }

    /**
     * {@code lhs || rhs}
     */
    public Block logicalOr(int line, Block lhs, Block rhs) {
        return new LogicalOpBlock(lhs,rhs,false);
    }

    /**
     * {@code lhs << rhs}
     */
    public Block leftShift(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "leftShift", rhs);
    }

    /**
     * {@code lhs <<= rhs}
     */
    public Block leftShiftEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "leftShift");
    }

    /**
     * {@code lhs >> rhs}
     */
    public Block rightShift(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "rightShift", rhs);
    }

    /**
     * {@code lhs >>= rhs}
     */
    public Block rightShiftEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "rightShift");
    }

    /**
     * {@code lhs >>> rhs}
     */
    public Block rightShiftUnsigned(int line, Block lhs, Block rhs) {
        return functionCall(line, lhs, "rightShiftUnsigned", rhs);
    }

    /**
     * {@code lhs >>>= rhs}
     */
    public Block rightShiftUnsignedEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "rightShiftUnsigned");
    }

    /**
     * {@code !b}
     */
    public Block not(int line, Block b) {
        return new NotBlock(b);
    }

    public Block bitwiseAnd(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"and",rhs);
    }

    public Block bitwiseAndEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "and");
    }

    public Block bitwiseOr(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"or",rhs);
    }

    public Block bitwiseOrEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "or");
    }

    public Block bitwiseXor(int line, Block lhs, Block rhs) {
        return functionCall(line,lhs,"xor",rhs);
    }

    public Block bitwiseXorEqual(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line), tags, lhs, rhs, "xor");
    }

    public Block bitwiseNegation(int line, Block b) {
        return staticCall(line,ScriptBytecodeAdapter.class,"bitwiseNegate",b);
    }

    /**
     * {@code ++x}
     */
    public Block prefixInc(int line, LValueBlock body) {
        return new ExcrementOperatorBlock(loc(line),tags,"next",true,body);
    }

    /**
     * {@code --x}
     */
    public Block prefixDec(int line, LValueBlock body) {
        return new ExcrementOperatorBlock(loc(line),tags,"previous",true,body);
    }

    /**
     * {@code x++}
     */
    public Block postfixInc(int line, LValueBlock body) {
        return new ExcrementOperatorBlock(loc(line),tags,"next",false,body);
    }

    /**
     * {@code x--}
     */
    public Block postfixDec(int line, LValueBlock body) {
        return new ExcrementOperatorBlock(loc(line),tags,"previous",false,body);
    }

    /**
     * Cast to type.
     *
     * @param coerce
     *      True for {@code exp as type} cast. false for {@code (type)exp} cast.
     */
    public Block cast(int line, Block block, Class type, boolean coerce) {
        return staticCall(line,ScriptBytecodeAdapter.class,
                coerce ? "asType" : "castToType",
                block,constant(type));
    }

    /**
     * @deprecated Just for compatibility with old scripts; prefer {@link #sandboxCastOrCoerce}
     */
    @Deprecated
    public Block sandboxCast(int line, Block block, Class<?> type, boolean ignoreAutoboxing, boolean strict) {
        return sandboxCastOrCoerce(line, block, type, ignoreAutoboxing, true, strict);
    }

    /**
     * Cast to type when {@link CastExpression#isCoerce} from {@link SandboxCpsTransformer}.
     */
    // TODO should ideally be defined in some sandbox-specific subtype of Builder
    public Block sandboxCastOrCoerce(int line, Block block, Class<?> type, boolean ignoreAutoboxing, boolean coerce, boolean strict) {
        return staticCall(line, Checker.class,
                "checkedCast",
                constant(type), block,
                constant(ignoreAutoboxing), constant(coerce), constant(strict));
    }

    public Block instanceOf(int line, Block value, Block type) {
        return functionCall(line,type,"isInstance",value);
    }

    /**
     * {@code LHS.name(...)}
     */
    public Block functionCall(int line, Block lhs, String name, Block... argExps) {
        return functionCall(line, lhs, constant(name), false, argExps);
    }

    public Block functionCall(int line, Block lhs, Block name, boolean safe, Block... argExps) {
        return new FunctionCallBlock(loc(line), tags, lhs, name, safe, argExps);
    }

    public Block assign(int line, LValueBlock lhs, Block rhs) {
        return new AssignmentBlock(loc(line),tags,lhs,rhs, null);
    }

    public LValueBlock property(int line, Block lhs, String property) {
        return property(line, lhs, constant(property), false);
    }

    public LValueBlock property(int line, Block lhs, Block property, boolean safe) {
        return new PropertyAccessBlock(loc(line), tags, lhs, property, safe);
    }

    public LValueBlock array(int line, Block lhs, Block index) {
        return new ArrayAccessBlock(loc(line),tags,lhs,index);
    }

    public LValueBlock attribute(int line, Block lhs, Block property, boolean safe) {
        return new AttributeAccessBlock(loc(line), tags, lhs, property, safe);
    }

    public LValueBlock staticField(int line, Class type, String name) {
        return new StaticFieldBlock(loc(line),type,name);
    }

    public Block setProperty(int line, Block lhs, String property, Block rhs) {
        return setProperty(line, lhs, constant(property), rhs);
    }

    public Block setProperty(int line, Block lhs, Block property, Block rhs) {
        return assign(line, property(line, lhs, property, false), rhs);
    }

    /**
     * Object instantiation.
     */
    public Block new_(int line, Class type, Block... argExps) {
        return new_(line,constant(type),argExps);
    }

    public Block new_(int line, Block type, Block... argExps) {
        return new FunctionCallBlock(loc(line), tags, type, constant("<init>"), false, argExps);
    }

    /**
     * Array instantiation like {@code new String[1][5]}
     */
    public Block newArray(int line, Class type, Block... argExps) {
        return new NewArrayBlock(loc(line),type,argExps);
    }

    /**
     * Array with initializers like {@code new Object[] {1, "two"}} which exists in Java but not Groovy.
     * Only used by {@link CpsDefaultGroovyMethods} and friends.
     */
    public Block newArrayFromInitializers(Block... args) {
        return new NewArrayFromInitializersBlock(args);
    }

    /**
     * {@code return exp;}
     */
    public Block return_(final Block exp) {
        return new ReturnBlock(exp);
    }

    /**
     * {@code [a,b,c,d]} that creates a List.
     */
    public Block list(Block... args) {
        return new ListBlock(args);
    }

    /**
     * {@code x..y} or {@code x..>y} to create a range
     */
    public Block range(int line, Block from, Block to, boolean inclusive) {
        return staticCall(line,ScriptBytecodeAdapter.class,"createRange",from,to,constant(inclusive));
    }

    public Block assert_(Block cond, Block msg, String sourceText) {
        return new AssertBlock(cond,msg,sourceText);
    }

    public Block assert_(Block cond, String sourceText) {
        return assert_(cond,null_(),sourceText);
    }

    /**
     * {@code "Foo bar zot ${x}"} kind of string
     */
    public Block gstring(int line, Block listOfValues, Block listOfStrings) {
        return new_(line, GStringImpl.class,
                cast(line,listOfValues, Object[].class,true),
                cast(line,listOfStrings,String[].class,true));
    }

    /**
     * @see #case_(int, Block, Block)
     */
    public Block switch_(String label, Block switchExp, Block defaultStmt, CaseExpression... caseExps) {
        return new SwitchBlock(label, switchExp, defaultStmt, Arrays.asList(caseExps));
    }

    public CaseExpression case_(int line, Block matcher, Block body) {
        return new CaseExpression(loc(line), matcher, body);
    }

    public Block yield(Object o) {
        return new YieldBlock(o);
    }

    private SourceLocation loc(int line) {
        return new SourceLocation(loc,line);
    }
}