/*
 * Copyright (c) 2016 Yahoo Inc.
 * Licensed under the terms of the Apache version 2.0 license.
 * See LICENSE file for terms.
 */

package com.yahoo.yqlplus.engine.internal.bytecode.types.gambit;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.yahoo.yqlplus.api.types.YQLCoreType;
import com.yahoo.yqlplus.engine.api.PropertyNotFoundException;
import com.yahoo.yqlplus.engine.internal.bytecode.ASMClassSource;
import com.yahoo.yqlplus.engine.internal.bytecode.types.ArrayTypeWidget;
import com.yahoo.yqlplus.engine.internal.compiler.*;
import com.yahoo.yqlplus.engine.internal.java.types.RecordMapWrapper;
import com.yahoo.yqlplus.engine.internal.operations.ArithmeticOperation;
import com.yahoo.yqlplus.engine.internal.operations.BinaryComparison;
import com.yahoo.yqlplus.engine.internal.plan.types.*;
import com.yahoo.yqlplus.engine.internal.plan.types.base.*;
import com.yahoo.yqlplus.language.parser.Location;
import com.yahoo.yqlplus.language.parser.ProgramCompileException;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class ExpressionHandler extends TypesHandler implements ScopedBuilder {
    protected LocalCodeChunk body;

    protected ExpressionHandler(ASMClassSource source) {
        super(source);
    }

    Label getStart() {
        return body.getStart();
    }

    Label getEnd() {
        return body.getEnd();
    }

    public RecordBuilder record() {
        return new ExpressionRecordBuilder();
    }

    public RecordBuilder dynamicRecord() {
        return new DynamicExpressionRecordBuilder();
    }

    AssignableValue evaluate(AssignableValue local, BytecodeExpression expr) {
        body.add(local.write(expr));
        return local;
    }

    @Override
    public AssignableValue allocate(TypeWidget type) {
        return body.allocate(type);
    }

    @Override
    public AssignableValue allocate(String name, TypeWidget type) {
        return body.allocate(name, type);
    }

    @Override
    public BytecodeExpression evaluateInto(String name, BytecodeExpression expr) {
        AssignableValue av = allocate(name, expr.getType());
        return evaluate(av, expr).read();
    }

    @Override
    public BytecodeExpression evaluateInto(BytecodeExpression expr) {
        if(expr instanceof LocalValue) {
            return expr;
        }
        AssignableValue av = allocate(expr.getType());
        return evaluate(av, expr).read();
    }

    @Override
    public void exec(final BytecodeSequence input) {
        body.add(input);
        if (input instanceof BytecodeExpression) {
            body.add(new PopSequence(((BytecodeExpression) input).getType()));
        }
    }

    @Override
    public void alias(String from, String to) {
        body.alias(from, to);
    }

    @Override
    public AssignableValue local(BytecodeExpression input) {
        AssignableValue av = allocate(input.getType());
        return evaluate(av, input);
    }

    @Override
    public void inc(final AssignableValue lv, final int count) {
        Preconditions.checkArgument(lv.getType().getJVMType().getSort() == Type.INT, "inc only applies to INT types: not %s", lv.getType().getJVMType());
        body.add(new BytecodeSequence() {
            @Override
            public void generate(CodeEmitter code) {
                code.inc(lv, count);
            }
        });
    }

    @Override
    public ScopeBuilder scope() {
        return new BlockAdapter(source, body.child());
    }

    @Override
    public LoopBuilder loop(BytecodeExpression test, BytecodeExpression result) {
        return new LoopAdapter(source, body, test, result);
    }

    @Override
    public IterateBuilder iterate(BytecodeExpression iterable) {
        return new IterateBuilderAdapter(source, body, iterable);
    }

    @Override
    public CaseBuilder createSwitch(BytecodeExpression expr) {
        throw new UnsupportedOperationException();
    }

    @Override
    public CaseBuilder createCase() {
        return new CaseAdapter(source, body);
    }

    @Override
    public IfBuilder createIf() {
        return new IfAdapter(source, body);
    }

    @Override
    public BytecodeExpression and(Location loc, BytecodeExpression... inputs) {
        return and(loc, Arrays.asList(inputs));
    }

    @Override
    public BytecodeExpression and(Location loc, final List<BytecodeExpression> inputs) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                Label done = new Label();
                Label isFalse = new Label();
                for (BytecodeExpression input : inputs) {
                    Label isTrue = new Label();
                    code.exec(input);
                    input.getType().getComparisionAdapter().coerceBoolean(code, isTrue, isFalse, isFalse);
                    mv.visitLabel(isTrue);
                }
                code.emitBooleanConstant(true);
                mv.visitJumpInsn(Opcodes.GOTO, done);
                mv.visitLabel(isFalse);
                code.emitBooleanConstant(false);
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public BytecodeExpression or(Location loc, BytecodeExpression... inputs) {
        return or(loc, Arrays.asList(inputs));
    }

    @Override
    public BytecodeExpression or(Location loc, final List<BytecodeExpression> inputs) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                Label done = new Label();
                Label isTrue = new Label();
                for (BytecodeExpression input : inputs) {
                    Label isFalse = new Label();
                    code.exec(input);
                    input.getType().getComparisionAdapter().coerceBoolean(code, isTrue, isFalse, isFalse);
                    mv.visitLabel(isFalse);
                }
                code.emitBooleanConstant(false);
                mv.visitJumpInsn(Opcodes.GOTO, done);
                mv.visitLabel(isTrue);
                code.emitBooleanConstant(true);
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public BytecodeExpression not(Location loc, final BytecodeExpression input) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                Label done = new Label();
                Label isTrue = new Label();
                Label isFalse = new Label();
                code.exec(input);
                input.getType().getComparisionAdapter().coerceBoolean(code, isTrue, isFalse, isFalse);
                mv.visitLabel(isFalse);
                code.emitBooleanConstant(true);
                mv.visitJumpInsn(Opcodes.GOTO, done);
                mv.visitLabel(isTrue);
                code.emitBooleanConstant(false);
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public BytecodeExpression bool(Location loc, final BytecodeExpression input) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                if (input.getType() == BaseTypeAdapter.BOOLEAN) {
                    code.exec(input);
                } else {
                    MethodVisitor mv = code.getMethodVisitor();
                    Label done = new Label();
                    Label isTrue = new Label();
                    Label isFalse = new Label();
                    code.exec(input);
                    input.getType().getComparisionAdapter().coerceBoolean(code, isTrue, isFalse, isFalse);
                    mv.visitLabel(isFalse);
                    code.emitBooleanConstant(false);
                    mv.visitJumpInsn(Opcodes.GOTO, done);
                    mv.visitLabel(isTrue);
                    code.emitBooleanConstant(true);
                    mv.visitLabel(done);
                }
            }
        };
    }

    @Override
    public BytecodeExpression isNull(Location location, final BytecodeExpression input) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                code.exec(input);
                // it's surprising to not evaluate even if we know input isn't nullable
                if (input.getType().isPrimitive()) {
                    code.pop(input.getType());
                    code.emitBooleanConstant(false);
                } else {
                    Label done = new Label();
                    Label isNull = new Label();
                    mv.visitJumpInsn(Opcodes.IFNULL, isNull);
                    code.emitBooleanConstant(false);
                    mv.visitJumpInsn(Opcodes.GOTO, done);
                    mv.visitLabel(isNull);
                    code.emitBooleanConstant(true);
                    mv.visitLabel(done);
                }
            }
        };
    }

    @Override
    public BytecodeExpression coalesce(Location loc, BytecodeExpression... inputs) {
        return coalesce(loc, Arrays.asList(inputs));
    }

    @Override
    public BytecodeExpression coalesce(Location loc, final List<BytecodeExpression> inputs) {
        List<TypeWidget> widgets = Lists.newArrayList();
        for (BytecodeExpression expr : inputs) {
            widgets.add(expr.getType());
        }
        TypeWidget output = unify(widgets);
        return new BaseTypeExpression(output) {
            @Override
            public void generate(CodeEmitter code) {
                Label done = new Label();
                MethodVisitor mv = code.getMethodVisitor();
                boolean lastNullable = true;
                for (BytecodeExpression expr : inputs) {
                    Label isNull = new Label();
                    code.exec(expr);
                    if (code.cast(getType(), expr.getType(), isNull)) {
                        mv.visitJumpInsn(Opcodes.GOTO, done);
                        mv.visitLabel(isNull);
                    } else {
                        lastNullable = false;
                        break;
                    }
                }
                if (lastNullable) {
                    mv.visitInsn(Opcodes.ACONST_NULL);
                }
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public AssignableValue local(String name) {
        return body.getLocal(name);
    }

    @Override
    public AssignableValue local(Location loc, String name) {
        return body.getLocal(name);
    }

    @Override
    public void set(Location loc, AssignableValue lv, BytecodeExpression expr) {
        body.add(lv.write(expr));
    }

    @Override
    public BytecodeExpression list(TypeWidget elementType) {
        return new ListTypeWidget(elementType).construct();
    }

    @Override
    public BytecodeExpression list(Location loc, BytecodeExpression... args) {
        return list(loc, Arrays.asList(args));
    }

    @Override
    public BytecodeExpression list(Location loc, final List<BytecodeExpression> args) {
        List<TypeWidget> types = Lists.newArrayList();
        for (BytecodeExpression e : args) {
            types.add(e.getType());
        }
        final TypeWidget unified = unify(types).boxed();
        final ListTypeWidget out = new ListTypeWidget(NotNullableTypeWidget.create(unified));
        return new BaseTypeExpression(out) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                code.exec(out.construct(constant(args.size())));
                for (BytecodeExpression expr : args) {
                    Label skip = new Label();
                    mv.visitInsn(Opcodes.DUP);
                    code.exec(expr);
                    final TypeWidget type = expr.getType();
                    boolean nullable = code.cast(unified, type, skip);
                    mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Collection.class), "add", Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)), true);
                    if (nullable) {
                        // we're either going to POP the DUPed List OR the result of add
                        mv.visitLabel(skip);
                    }
                    mv.visitInsn(Opcodes.POP);
                }
            }
        };
    }

    @Override
    public BytecodeExpression newArray(Location loc, final TypeWidget elementType, final BytecodeExpression count) {
        return new BaseTypeExpression(NotNullableTypeWidget.create(new ArrayTypeWidget(Type.getType("[" + elementType.getJVMType().getDescriptor()), elementType))) {
            @Override
            public void generate(CodeEmitter code) {
                code.emitNewArray(elementType, count);
            }
        };
    }

    @Override
    public BytecodeExpression array(Location loc, TypeWidget elementType, BytecodeExpression... args) {
        return array(loc, elementType, Arrays.asList(args));
    }

    @Override
    public BytecodeExpression array(Location loc, TypeWidget elementType, final List<BytecodeExpression> args) {
        final BytecodeExpression create = newArray(loc, elementType, constant(args.size()));
        return new BaseTypeExpression(create.getType()) {
            @Override
            public void generate(CodeEmitter code) {
                CodeEmitter tmp = code.createScope();
                IndexAdapter idx = getType().getIndexAdapter();
                AssignableValue local = tmp.allocate(create);
                for (int i = 0; i < args.size(); ++i) {
                    tmp.exec(idx.index(local, constant(i)).write(args.get(i)));
                }
                tmp.exec(local.read());
                tmp.endScope();

            }
        };
    }

    @Override
    public BytecodeExpression invokeExact(Location loc, String methodName, Class<?> owner, TypeWidget returnType, BytecodeExpression... args) {
        return invokeExact(loc, methodName, owner, returnType, args == null ? ImmutableList.<BytecodeExpression>of() : Arrays.asList(args));
    }

    @Override
    public BytecodeExpression invokeExact(Location loc, String methodName, Class<?> owner, TypeWidget returnType, List<BytecodeExpression> args) {
        return ExactInvocation.boundInvoke(owner.isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
                methodName, adapt(owner, false), returnType, args).invoke(loc);
    }

    @Override
    public BytecodeExpression invokeStatic(Location loc, String methodName, Class<?> owner, TypeWidget returnType, BytecodeExpression... args) {
        return invokeStatic(loc, methodName, owner, returnType, args == null ? ImmutableList.<BytecodeExpression>of() : Arrays.asList(args));
    }

    @Override
    public BytecodeExpression invokeStatic(Location loc, String methodName, Class<?> owner, TypeWidget returnType, List<BytecodeExpression> args) {
        return invoke(loc, ExactInvocation.boundInvoke(Opcodes.INVOKESTATIC, methodName, adapt(owner, false), returnType, args), ImmutableList.<BytecodeExpression>of());
    }

    @Override
    public BytecodeExpression invoke(Location loc, BytecodeExpression target, String operationName, BytecodeExpression... args) {
        return invoke(loc, target, operationName, args != null ? Arrays.asList(args) : ImmutableList.<BytecodeExpression>of());
    }

    @Override
    public BytecodeExpression invoke(Location loc, BytecodeExpression target, String operationName, List<BytecodeExpression> args) {
        TypeWidget widget = target.getType();
        return widget.invoke(target, operationName, args);
    }

    @Override
    public BytecodeExpression call(Location location, TypeWidget outputType, String name, List<BytecodeExpression> argumentExprs) {
        TypeWidget widget = argumentExprs.get(0).getType();
        return widget.invoke(argumentExprs.get(0), outputType, name, argumentExprs.subList(1, argumentExprs.size()));
    }

    @Override
    public Invocable constructor(TypeWidget type, TypeWidget... argumentTypes) {
        return constructor(type, argumentTypes == null ? ImmutableList.<TypeWidget>of() : Arrays.asList(argumentTypes));
    }

    @Override
    public Invocable constructor(final TypeWidget type, List<TypeWidget> argumentTypes) {
        return new BytecodeInvocable(type, argumentTypes) {
            @Override
            protected void generate(Location loc, CodeEmitter code, List<BytecodeExpression> args) {
                code.exec(type.construct(args.toArray(new BytecodeExpression[args.size()])));
            }
        };
    }

    @Override
    public BytecodeExpression invoke(Location loc, Invocable invocable, BytecodeExpression... args) {
        return invocable.invoke(loc, args);
    }

    @Override
    public BytecodeExpression invoke(Location loc, Invocable invocable, List<BytecodeExpression> args) {
        Iterator<TypeWidget> types = invocable.getArgumentTypes().iterator();
        List<BytecodeExpression> castArgs = Lists.newArrayListWithCapacity(args.size());
        for (BytecodeExpression arg : args) {
            castArgs.add(cast(loc, types.next(), arg));
        }
        return invocable.invoke(loc, castArgs);
    }

    @Override
    public BytecodeExpression negate(Location loc, BytecodeExpression input) {
        return new BytecodeNegateExpression(loc, input);
    }

    @Override
    public BytecodeExpression arithmetic(Location loc, ArithmeticOperation op, BytecodeExpression left, BytecodeExpression right) {
        // a bit of a hack; should not need to go to dynamic invocation for this unless one arg is ANY
        TypeWidget unified = source.getValueTypeAdapter().unifyTypes(ImmutableList.of(left.getType(), right.getType()));
        // TODO: move type unification here!
        return new BytecodeArithmeticExpression(loc, unified, op, left, right);
    }

    @Override
    public BytecodeExpression contains(Location loc, BytecodeExpression left, BytecodeExpression right) {
        throw new UnsupportedOperationException();
    }

    @Override
    public BytecodeExpression matches(Location loc, final BytecodeExpression left, final BytecodeExpression right) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                Label done = new Label();
                Label anyIsNull = new Label();
                CodeEmitter.BinaryCoercion coerce = code.binaryCoercion(right, Pattern.class, left, CharSequence.class, anyIsNull, anyIsNull, anyIsNull);
                MethodVisitor mv = code.getMethodVisitor();
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Pattern.class), "matcher",
                        Type.getMethodDescriptor(Type.getType(Matcher.class), Type.getType(CharSequence.class)), false);
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Matcher.class), "matches",
                        Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false);
                if (coerce.leftNullable || coerce.rightNullable) {
                    mv.visitJumpInsn(Opcodes.GOTO, done);
                    mv.visitLabel(anyIsNull);
                    mv.visitInsn(Opcodes.ICONST_0);
                    mv.visitLabel(done);
                }
            }
        };
    }

    @Override
    public BytecodeExpression in(Location loc, final BytecodeExpression left, final BytecodeExpression right) {
        return new BaseTypeExpression(BaseTypeAdapter.BOOLEAN) {
            @Override
            public void generate(CodeEmitter code) {
                Label done = new Label();
                Label anyIsNull = new Label();
                CodeEmitter.BinaryCoercion coerce = code.binaryCoercion(right, Collection.class, left, Object.class, anyIsNull, anyIsNull, anyIsNull);
                MethodVisitor mv = code.getMethodVisitor();
                mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Collection.class), "contains",
                        Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)), true);
                if (coerce.leftNullable || coerce.rightNullable) {
                    mv.visitJumpInsn(Opcodes.GOTO, done);
                    mv.visitLabel(anyIsNull);
                    mv.visitInsn(Opcodes.ICONST_0);
                    mv.visitLabel(done);
                }
            }
        };
    }

    @Override
    public BytecodeExpression eq(Location loc, BytecodeExpression left, BytecodeExpression right) {
        return new EqualsExpression(loc, left, right, false);
    }

    @Override
    public BytecodeExpression neq(Location loc, BytecodeExpression left, BytecodeExpression right) {
        return new EqualsExpression(loc, left, right, true);
    }

    @Override
    public BytecodeExpression compare(Location loc, BytecodeExpression left, BytecodeExpression right) {
        return new CompareExpression(loc, left, right);
    }

    @Override
    public BytecodeExpression compare(Location loc, BinaryComparison booleanComparison, BytecodeExpression left, BytecodeExpression right) {
        return new BooleanCompareExpression(loc, left, right, booleanComparison);
    }

    @Override
    public BytecodeExpression composeCompare(List<BytecodeExpression> compares) {
        return new MulticompareExpression(compares);
    }

    public BytecodeExpression transform(Location location, BytecodeExpression iterable, Invocable function) {
        ScopeBuilder scope = scope();
        IterateBuilder it = scope.iterate(iterable);
        BytecodeExpression list = scope.evaluateInto(list(function.getReturnType()));
        it.exec(it.findExactInvoker(Collection.class, "add", BaseTypeAdapter.BOOLEAN, AnyTypeWidget.getInstance()).invoke(location, list, it.invoke(location, function, it.getItem())));
        return scope.complete(it.build(list));
    }

    @Override
    public BytecodeExpression first(Location location, BytecodeExpression inputExpr) {
        if (!inputExpr.getType().isIterable()) {
            throw new ProgramCompileException(location, "Unable to iterate argument to first: %s", inputExpr.getType().getTypeName());
        }
        return inputExpr.getType().getIterableAdapter().first(inputExpr);
    }

    @Override
    public BytecodeExpression length(Location location, BytecodeExpression inputExpr) {
        if (!inputExpr.getType().isIndexable()) {
            throw new ProgramCompileException(location, "Argument to length not indexable: %s", inputExpr.getType().getTypeName());
        }
        return inputExpr.getType().getIndexAdapter().length(inputExpr);
    }

    @Override
    public BytecodeExpression scatter(Location location, BytecodeExpression runtime, BytecodeExpression iterable, CallableInvocable function) {
        BytecodeExpression callables = transform(location, iterable, function);
        TypeWidget resultType;
        boolean async = false;
        if (function.getResultType().isPromise()) {
            async = true;
            resultType = new ListTypeWidget(function.getResultType().getPromiseAdapter().getResultType());
        } else {
            resultType = new ListTypeWidget(function.getResultType());
        }
        ListenableFutureResultType futureResultType = new ListenableFutureResultType(resultType);
        return ExactInvocation.boundInvoke(Opcodes.INVOKEINTERFACE, async ? "scatterAsync" : "scatter", adapt(GambitRuntime.class, false),
                futureResultType,
                runtime,
                callables).invoke(location);
    }

    @Override
    public BytecodeExpression fork(Location location, BytecodeExpression runtime, CallableInvocable function, BytecodeExpression... arguments) {
        return fork(location, runtime, function, arguments == null ? ImmutableList.<BytecodeExpression>of() : Arrays.asList(arguments));
    }

    @Override
    public BytecodeExpression fork(Location location, BytecodeExpression runtime, CallableInvocable function, List<BytecodeExpression> arguments) {
        TypeWidget resultType;
        boolean async = false;
        if (function.getResultType().isPromise()) {
            async = true;
            resultType = function.getResultType().getPromiseAdapter().getResultType();
        } else {
            resultType = function.getResultType();
        }
        ListenableFutureResultType futureResultType = new ListenableFutureResultType(resultType);
        return ExactInvocation.boundInvoke(Opcodes.INVOKEINTERFACE, async ? "forkAsync" : "fork", adapt(GambitRuntime.class, false),
                futureResultType,
                runtime,
                cast(adapt(Callable.class, false), function.invoke(location, arguments))).invoke(location);
    }

    @Override
    public BytecodeExpression propertyValue(Location loc, BytecodeExpression target, String propertyName) {
        if (!target.getType().hasProperties()) {
            throw new ProgramCompileException(loc, "Cannot reference %s.%s", target.getType().getJVMType(), propertyName);
        }
        try {
            if (target.getType().isNullable()) {
                ScopeBuilder guard = scope();
                BytecodeExpression tgt = guard.evaluateInto(target);
                final AssignableValue property = target.getType().getPropertyAdapter().property(new NullCheckedEvaluatedExpression(tgt), propertyName);
                return guard.complete(guard.guarded(tgt, property));

            }
            return target.getType().getPropertyAdapter().property(target, propertyName);
        } catch (PropertyNotFoundException e) {
            throw new ProgramCompileException(loc, e.getMessage());
        }
    }

    @Override
    public BytecodeExpression indexValue(Location loc, BytecodeExpression target, BytecodeExpression index) {
        if (target.getType().isNullable()) {
            ScopeBuilder guard = scope();
            BytecodeExpression tgt = guard.evaluateInto(target);
            return guard.complete(guard.guarded(tgt, target.getType().getIndexAdapter().index(new NullCheckedEvaluatedExpression(tgt), index)));

        }
        return target.getType().getIndexAdapter().index(target, index);
    }

    @Override
    public BytecodeExpression guarded(final BytecodeExpression target, final BytecodeExpression ifTargetIsNotNull) {
        Preconditions.checkNotNull(target);
        Preconditions.checkNotNull(ifTargetIsNotNull);
        if (!target.getType().isNullable()) {
            return ifTargetIsNotNull;
        }
        return new NullGuardedExpression(target, ifTargetIsNotNull);
    }

    @Override
    public BytecodeExpression guarded(final BytecodeExpression target, final BytecodeExpression ifTargetIsNotNull, final BytecodeExpression ifTargetIsNull) {
        if (!target.getType().isNullable()) {
            return ifTargetIsNotNull;
        }
        return new BaseTypeExpression(unify(ifTargetIsNotNull.getType(), ifTargetIsNull.getType())) {
            @Override
            public void generate(CodeEmitter code) {
                final MethodVisitor mv = code.getMethodVisitor();
                Label isNull = new Label();
                Label done = new Label();
                code.exec(target);
                code.nullTest(target.getType(), isNull);
                code.pop(target.getType());
                code.exec(ifTargetIsNotNull);
                code.cast(getType(), ifTargetIsNotNull.getType(), isNull);
                mv.visitJumpInsn(Opcodes.GOTO, done);
                mv.visitLabel(isNull);
                code.exec(ifTargetIsNull);
                code.cast(getType(), ifTargetIsNull.getType());
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public AssignableValue propertyRef(Location loc, BytecodeExpression target, String propertyName) {
        if (!target.getType().hasProperties()) {
            throw new ProgramCompileException(loc, "Cannot reference %s.%s", target.getType().getJVMType(), propertyName);
        }
        return target.getType().getPropertyAdapter().property(target, propertyName);
    }

    @Override
    public AssignableValue indexRef(Location loc, BytecodeExpression target, BytecodeExpression index) {
        return target.getType().getIndexAdapter().index(target, index);
    }

    @Override
    public BytecodeExpression cast(Location loc, TypeWidget type, BytecodeExpression input) {
        return new BytecodeCastExpression(type, input);
    }

    public BytecodeExpression cast(TypeWidget type, BytecodeExpression input) {
        return new BytecodeCastExpression(type, input);
    }

    @Override
    public BytecodeExpression fallback(Location loc, final BytecodeExpression primary, final BytecodeExpression caught) {
        TypeWidget unified = unify(primary.getType(), caught.getType());
        return new BaseTypeExpression(unified) {
            @Override
            public void generate(CodeEmitter code) {
                MethodVisitor mv = code.getMethodVisitor();
                final Label start = new Label();
                final Label endCatch = new Label();
                final Label handler = new Label();
                Label done = new Label();
                // this probably should not be catching throwable and instead should be catching Exception
                // or permit certain Errors through only
                mv.visitTryCatchBlock(start, endCatch, handler, "java/lang/Throwable");
                mv.visitLabel(start);
                code.exec(primary);
                Label isNull = new Label();
                boolean maybeNull = code.cast(getType(), primary.getType(), isNull);
                mv.visitJumpInsn(Opcodes.GOTO, done);
                mv.visitLabel(endCatch);
                mv.visitLabel(handler);
                mv.visitInsn(Opcodes.POP);
                if (maybeNull) {
                    mv.visitLabel(isNull);
                }
                code.exec(caught);
                code.cast(getType(), caught.getType());
                mv.visitLabel(done);
            }
        };
    }

    @Override
    public BytecodeExpression resolve(Location loc, BytecodeExpression timeout, BytecodeExpression promise) {
        if (promise.getType().isResult()) {
            return promise.getType().getResultAdapter().resolve(promise);
        } else if (promise.getType().isPromise()) {
            return promise.getType().getPromiseAdapter().resolve(this, timeout, promise);
        } else {
            return promise;
        }
    }

    @Override
    public BytecodeExpression resolveLater(Location loc, BytecodeExpression timeout, BytecodeExpression promise, Invocable callback) {
        throw new UnsupportedOperationException();
    }

    @Override
    public BytecodeExpression resolveLater(Location loc, BytecodeExpression timeout, BytecodeExpression promise, Invocable success, Invocable failure) {
        throw new UnsupportedOperationException();
    }

    @Override
    public CatchBuilder tryCatchFinally() {
        return new BytecodeCatchBuilder(source, body);
    }

    @Override
    public BytecodeExpression evaluateTryCatch(Location loc, final BytecodeExpression expr) {
        // TODO: should we initialize the output value with a null?
        final TypeWidget resultType = resultTypeFor(expr.getType());
        CatchBuilder tryCatch = tryCatchFinally();
        AssignableValue resultValue = body.allocate(resultType);
        ScopedBuilder body = tryCatch.body();
        body.set(loc, resultValue, resultType.getResultAdapter().createSuccess(expr));
        ScopedBuilder catchBlock = tryCatch.on("$e", Throwable.class);
        catchBlock.set(loc, resultValue, resultType.getResultAdapter().createFailureThrowable(catchBlock.local("$e")));
        exec(tryCatch.build());
        return resultValue;
    }

    @Override
    public ScopeBuilder block() {
        return new BlockAdapter(source, body.block());
    }

    @Override
    public ScopeBuilder point() {
        return new BlockAdapter(source, body.point());
    }

    @Override
    public LocalCodeChunk getCode() {
        return body;
    }

    private static class NullGuardedExpression extends BaseTypeExpression {
        private final BytecodeExpression ifTargetIsNotNull;
        private final BytecodeExpression target;

        public NullGuardedExpression(BytecodeExpression target, BytecodeExpression ifTargetIsNotNull) {
            super(NullableTypeWidget.create(ifTargetIsNotNull.getType().boxed()));
            this.ifTargetIsNotNull = ifTargetIsNotNull;
            this.target = target;
        }

        @Override
        public void generate(CodeEmitter code) {
            final MethodVisitor mv = code.getMethodVisitor();
            Label isNull = new Label();
            Label done = new Label();
            code.exec(target);
            mv.visitJumpInsn(Opcodes.IFNULL, isNull);
            code.exec(ifTargetIsNotNull);
            code.cast(getType(), ifTargetIsNotNull.getType());
            mv.visitJumpInsn(Opcodes.GOTO, done);
            mv.visitLabel(isNull);
            mv.visitInsn(Opcodes.ACONST_NULL);
            mv.visitLabel(done);
        }
    }

    private static final TypeWidget mapFieldWriter = new BaseTypeWidget(Type.getType(MapFieldWriter.class)) {
        @Override
        public YQLCoreType getValueCoreType() {
            return YQLCoreType.OBJECT;
        }

        @Override
        protected SerializationAdapter getJsonSerializationAdapter() {
            throw new UnsupportedOperationException();
        }
    };
    private static final TypeWidget mapType = new MapTypeWidget(Type.getType(RecordMapWrapper.class), BaseTypeAdapter.STRING, BaseTypeAdapter.ANY);


    private class DynamicOperation {
        final String fieldName;
        final BytecodeExpression value;

        public DynamicOperation(String fieldName, BytecodeExpression value) {
            this.fieldName = fieldName;
            this.value = value;
        }
    }

    private class DynamicExpressionRecordBuilder implements RecordBuilder {
        private final List<DynamicOperation> operationNodes = Lists.newArrayList();

        @Override
        public RecordBuilder add(Location loc, String fieldName, BytecodeExpression input) {
            operationNodes.add(new DynamicOperation(fieldName, input));
            return this;
        }

        @Override
        public RecordBuilder merge(Location loc, BytecodeExpression recordType) {
            operationNodes.add(new DynamicOperation("", recordType));
            return this;
        }

        @Override
        public BytecodeExpression build() {
            return new BaseTypeExpression(mapType) {
                @Override
                public void generate(CodeEmitter code) {
                    AssignableValue map = code.allocate(mapType.construct());
                    PropertyAdapter adapter = map.getType().getPropertyAdapter();
                    AssignableValue writer = code.allocate(mapFieldWriter.construct(new BytecodeCastExpression(new MapTypeWidget(Type.getType(Map.class), BaseTypeAdapter.STRING, BaseTypeAdapter.ANY), map)));
                    for(DynamicOperation op : operationNodes) {
                        if("".equals(op.fieldName)) {
                            BytecodeExpression value = op.value;
                            code.exec(value.getType().getPropertyAdapter().mergeIntoFieldWriter(value, writer));
                        } else {
                            String fieldName = op.fieldName;
                            BytecodeExpression value = op.value;
                            code.exec(adapter.property(map, fieldName).write(value));
                        }
                    }
                    code.exec(map.read());
                }
            };
        }
    }
    
    private static boolean isJavaFieldName(String fieldName) {
        if (fieldName.isEmpty()) {
            return false;
        } else {
            if (!Character.isJavaIdentifierStart(fieldName.charAt(0))) {
                return false;
            }
            if (fieldName.length() == 1) {
                return true;
            } else {
                for (int i = 1; i < fieldName.length(); i++) {
                    if (!Character.isJavaIdentifierPart(fieldName.charAt(i))) {
                        return false;
                    }
                }
                return true;
            }
        }     
    }

    private class ExpressionRecordBuilder implements RecordBuilder {
        private boolean dynamic = false;
        private RecordBuilder dynamicBuilder = null;

        private final Map<String, BytecodeExpression> fieldSettings = Maps.newLinkedHashMap();
        private final StructBuilder staticStructBuilder = createStruct();

        private void initDynamicBuilder() {
            // reset and convert ourselves to dynamic
            dynamic = true;
            dynamicBuilder = new DynamicExpressionRecordBuilder();
            // merge all of our existing properties to the dynamic builder
            for (Map.Entry<String, BytecodeExpression> entry:fieldSettings.entrySet()) {
                dynamicBuilder.add(Location.NONE, entry.getKey(), entry.getValue());
            }              
        }
        
        @Override
        public RecordBuilder add(Location loc, String fieldName, BytecodeExpression input) {
            if(dynamic) {
                dynamicBuilder.add(loc, fieldName, input);
                return this;
            }
            if (!isJavaFieldName(fieldName)) {
                initDynamicBuilder();
                dynamicBuilder.add(loc, fieldName, input);
                return this;
            }
            fieldSettings.put(fieldName, input);
            staticStructBuilder.add(fieldName, input.getType());
            return this;
        }

        @Override
        public RecordBuilder merge(Location loc, BytecodeExpression recordType) {
            if(dynamic) {
                dynamicBuilder.merge(loc, recordType);
                return this;
            }
            TypeWidget inputType = recordType.getType();
            if(!inputType.hasProperties()) {
                throw new UnsupportedOperationException("RecordBuilder.merge must take an argument with properties (e.g. a struct/record)");
            }
            PropertyAdapter inputProperties = inputType.getPropertyAdapter();
            if(!inputProperties.isClosed()) {
                initDynamicBuilder();
                dynamicBuilder.merge(loc, recordType);
                return this;
            }
            for(PropertyAdapter.Property property : inputProperties.getProperties()) {

                add(loc, property.name, guarded(recordType, inputProperties.property(recordType, property.name)));
            }
            return this;
        }

        @Override
        public BytecodeExpression build() {
            if(dynamic) {
                return dynamicBuilder.build();
            }
            return staticStructBuilder.build()
                    .getPropertyAdapter()
                    .construct(fieldSettings);
        }
    }
}