/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.facebook.presto.sql.gen;

import com.facebook.presto.bytecode.BytecodeBlock;
import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.ClassDefinition;
import com.facebook.presto.bytecode.MethodDefinition;
import com.facebook.presto.bytecode.Parameter;
import com.facebook.presto.bytecode.ParameterizedType;
import com.facebook.presto.bytecode.Scope;
import com.facebook.presto.bytecode.Variable;
import com.facebook.presto.bytecode.control.TryCatch;
import com.facebook.presto.metadata.Signature;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.type.Type;
import com.facebook.presto.sql.relational.CallExpression;
import com.facebook.presto.sql.relational.RowExpression;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Primitives;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.util.List;
import java.util.Map;

import static com.facebook.presto.bytecode.Access.PUBLIC;
import static com.facebook.presto.bytecode.Access.a;
import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantBoolean;
import static com.facebook.presto.spi.StandardErrorCode.DIVISION_BY_ZERO;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static com.facebook.presto.spi.StandardErrorCode.NUMERIC_VALUE_OUT_OF_RANGE;
import static com.facebook.presto.sql.gen.BytecodeUtils.boxPrimitiveIfNecessary;
import static com.facebook.presto.sql.gen.BytecodeUtils.invoke;
import static com.facebook.presto.sql.gen.BytecodeUtils.unboxPrimitiveIfNecessary;
import static com.facebook.presto.util.Reflection.methodHandle;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.getOnlyElement;
import static java.lang.invoke.MethodType.methodType;

public class TryCodeGenerator
        implements BytecodeGenerator
{
    private static final String EXCEPTION_HANDLER_NAME = "tryExpressionExceptionHandler";
    private static final MethodHandle EXCEPTION_HANDLER = methodHandle(TryCodeGenerator.class, EXCEPTION_HANDLER_NAME, PrestoException.class);

    private final Map<CallExpression, MethodDefinition> tryMethodsMap;

    public TryCodeGenerator(Map<CallExpression, MethodDefinition> tryMethodsMap)
    {
        this.tryMethodsMap = tryMethodsMap;
    }

    @Override
    public BytecodeNode generateExpression(Signature signature, BytecodeGeneratorContext context, Type returnType, List<RowExpression> arguments)
    {
        checkArgument(arguments.size() == 1, "try methods only contain a single expression");
        checkArgument(getOnlyElement(arguments) instanceof CallExpression, "try methods must contain a call expression");

        CallExpression innerCallExpression = (CallExpression) getOnlyElement(arguments);
        checkState(tryMethodsMap.containsKey(innerCallExpression), "try methods map does not contain this try call");

        MethodDefinition definition = tryMethodsMap.get(innerCallExpression);

        ImmutableList<Variable> invokeArguments = definition.getParameters().stream()
                .map(parameter -> context.getScope().getVariable(parameter.getName()))
                .collect(toImmutableList());

        return new BytecodeBlock()
                .append(context.getScope().getThis().invoke(definition, invokeArguments))
                .append(unboxPrimitiveIfNecessary(context.getScope(), Primitives.wrap(innerCallExpression.getType().getJavaType())));
    }

    public static MethodDefinition defineTryMethod(
            RowExpressionCompiler innerExpressionCompiler,
            ClassDefinition classDefinition,
            String methodName,
            List<Parameter> inputParameters,
            Class<?> returnType,
            RowExpression innerRowExpression,
            CallSiteBinder callSiteBinder)
    {
        MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), methodName, type(returnType), inputParameters);
        Scope calleeMethodScope = method.getScope();

        Variable wasNull = calleeMethodScope.declareVariable(boolean.class, "wasNull");
        BytecodeNode innerExpression = innerExpressionCompiler.compile(innerRowExpression, calleeMethodScope);

        MethodType exceptionHandlerType = methodType(returnType, PrestoException.class);
        MethodHandle exceptionHandler = EXCEPTION_HANDLER.asType(exceptionHandlerType);
        Binding binding = callSiteBinder.bind(exceptionHandler);

        method.comment("Try projection: %s", innerRowExpression.toString());
        method.getBody()
                .append(wasNull.set(constantBoolean(false)))
                .append(new TryCatch(
                        new BytecodeBlock()
                                .append(innerExpression)
                                .append(boxPrimitiveIfNecessary(calleeMethodScope, returnType)),
                        new BytecodeBlock()
                                .append(invoke(binding, EXCEPTION_HANDLER_NAME)),
                        ParameterizedType.type(PrestoException.class)))
                .ret(returnType);

        return method;
    }

    public static Object tryExpressionExceptionHandler(PrestoException e)
            throws PrestoException
    {
        int errorCode = e.getErrorCode().getCode();
        if (errorCode == DIVISION_BY_ZERO.toErrorCode().getCode()
                || errorCode == INVALID_CAST_ARGUMENT.toErrorCode().getCode()
                || errorCode == INVALID_FUNCTION_ARGUMENT.toErrorCode().getCode()
                || errorCode == NUMERIC_VALUE_OUT_OF_RANGE.toErrorCode().getCode()) {
            return null;
        }

        throw e;
    }
}
