/*
 * 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.bytecode.expression;

import com.facebook.presto.bytecode.BytecodeBlock;
import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.MethodGenerationContext;
import com.facebook.presto.bytecode.OpCode;
import com.facebook.presto.bytecode.ParameterizedType;
import com.google.common.collect.ImmutableList;

import java.util.List;

import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.primitives.Primitives.wrap;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

class CastBytecodeExpression
        extends BytecodeExpression
{
    private static final ParameterizedType OBJECT_TYPE = type(Object.class);

    private final BytecodeExpression instance;

    public CastBytecodeExpression(BytecodeExpression instance, ParameterizedType type)
    {
        super(type);

        this.instance = requireNonNull(instance, "instance is null");

        checkArgument(type.getPrimitiveType() != void.class, "Type %s can not be cast to %s", instance.getType(), type);

        // Call generateBytecode to run the validation logic. The result is thrown away.
        // Duplicating the validation logic here is error-prone and introduces duplicate code.
        generateBytecode(instance.getType(), getType());
    }

    @Override
    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
    {
        return new BytecodeBlock()
                .append(instance.getBytecode(generationContext))
                .append(generateBytecode(instance.getType(), getType()));
    }

    private static BytecodeBlock generateBytecode(ParameterizedType sourceType, ParameterizedType targetType)
    {
        BytecodeBlock block = new BytecodeBlock();

        switch (getTypeKind(sourceType)) {
            case PRIMITIVE:
                switch (getTypeKind(targetType)) {
                    case PRIMITIVE:
                        castPrimitiveToPrimitive(block, sourceType.getPrimitiveType(), targetType.getPrimitiveType());
                        return block;
                    case BOXED_PRIMITVE:
                        checkArgument(sourceType.getPrimitiveType() == unwrapPrimitiveType(targetType), "Type %s can not be cast to %s", sourceType, targetType);
                        return block.invokeStatic(targetType, "valueOf", targetType, sourceType);
                    case OTHER:
                        checkArgument(OBJECT_TYPE.equals(targetType), "Type %s can not be cast to %s", sourceType, targetType);
                        Class<?> sourceClass = sourceType.getPrimitiveType();
                        return block
                                .invokeStatic(wrap(sourceClass), "valueOf", wrap(sourceClass), sourceClass)
                                .checkCast(targetType);
                }
            case BOXED_PRIMITVE:
                switch (getTypeKind(targetType)) {
                    case PRIMITIVE:
                        checkArgument(unwrapPrimitiveType(sourceType) == targetType.getPrimitiveType(), "Type %s can not be cast to %s", sourceType, targetType);
                        return block.invokeVirtual(sourceType, targetType.getPrimitiveType().getSimpleName() + "Value", targetType);
                    case BOXED_PRIMITVE:
                        checkArgument(sourceType.equals(targetType), "Type %s can not be cast to %s", sourceType, targetType);
                        return block;
                    case OTHER:
                        return block.checkCast(targetType);
                }
            case OTHER:
                switch (getTypeKind(targetType)) {
                    case PRIMITIVE:
                        checkArgument(OBJECT_TYPE.equals(sourceType), "Type %s can not be cast to %s", sourceType, targetType);
                        return block
                                .checkCast(wrap(targetType.getPrimitiveType()))
                                .invokeVirtual(wrap(targetType.getPrimitiveType()), targetType.getPrimitiveType().getSimpleName() + "Value", targetType.getPrimitiveType());
                    case BOXED_PRIMITVE:
                    case OTHER:
                        return block.checkCast(targetType);
                }
        }
        throw new UnsupportedOperationException("unexpected enum value");
    }

    private static BytecodeBlock castPrimitiveToPrimitive(BytecodeBlock block, Class<?> sourceType, Class<?> targetType)
    {
        if (sourceType == boolean.class) {
            if (targetType == boolean.class) {
                return block;
            }
        }
        if (sourceType == byte.class) {
            if (targetType == byte.class) {
                return block;
            }
            if (targetType == char.class) {
                return block;
            }
            if (targetType == short.class) {
                return block;
            }
            if (targetType == int.class) {
                return block;
            }
            if (targetType == long.class) {
                return block.append(OpCode.I2L);
            }
            if (targetType == float.class) {
                return block.append(OpCode.I2F);
            }
            if (targetType == double.class) {
                return block.append(OpCode.I2D);
            }
        }
        if (sourceType == char.class) {
            if (targetType == byte.class) {
                return block.append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block;
            }
            if (targetType == short.class) {
                return block;
            }
            if (targetType == int.class) {
                return block;
            }
            if (targetType == long.class) {
                return block.append(OpCode.I2L);
            }
            if (targetType == float.class) {
                return block.append(OpCode.I2F);
            }
            if (targetType == double.class) {
                return block.append(OpCode.I2D);
            }
        }
        if (sourceType == short.class) {
            if (targetType == byte.class) {
                return block.append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block.append(OpCode.I2C);
            }
            if (targetType == short.class) {
                return block;
            }
            if (targetType == int.class) {
                return block;
            }
            if (targetType == long.class) {
                return block.append(OpCode.I2L);
            }
            if (targetType == float.class) {
                return block.append(OpCode.I2F);
            }
            if (targetType == double.class) {
                return block.append(OpCode.I2D);
            }
        }
        if (sourceType == int.class) {
            if (targetType == boolean.class) {
                return block;
            }
            if (targetType == byte.class) {
                return block.append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block.append(OpCode.I2C);
            }
            if (targetType == short.class) {
                return block.append(OpCode.I2S);
            }
            if (targetType == int.class) {
                return block;
            }
            if (targetType == long.class) {
                return block.append(OpCode.I2L);
            }
            if (targetType == float.class) {
                return block.append(OpCode.I2F);
            }
            if (targetType == double.class) {
                return block.append(OpCode.I2D);
            }
        }
        if (sourceType == long.class) {
            if (targetType == byte.class) {
                return block.append(OpCode.L2I).append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block.append(OpCode.L2I).append(OpCode.I2C);
            }
            if (targetType == short.class) {
                return block.append(OpCode.L2I).append(OpCode.I2S);
            }
            if (targetType == int.class) {
                return block.append(OpCode.L2I);
            }
            if (targetType == long.class) {
                return block;
            }
            if (targetType == float.class) {
                return block.append(OpCode.L2F);
            }
            if (targetType == double.class) {
                return block.append(OpCode.L2D);
            }
        }
        if (sourceType == float.class) {
            if (targetType == byte.class) {
                return block.append(OpCode.F2I).append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block.append(OpCode.F2I).append(OpCode.I2C);
            }
            if (targetType == short.class) {
                return block.append(OpCode.F2I).append(OpCode.I2S);
            }
            if (targetType == int.class) {
                return block.append(OpCode.F2I);
            }
            if (targetType == long.class) {
                return block.append(OpCode.F2L);
            }
            if (targetType == float.class) {
                return block;
            }
            if (targetType == double.class) {
                return block.append(OpCode.F2D);
            }
        }
        if (sourceType == double.class) {
            if (targetType == byte.class) {
                return block.append(OpCode.D2I).append(OpCode.I2B);
            }
            if (targetType == char.class) {
                return block.append(OpCode.D2I).append(OpCode.I2C);
            }
            if (targetType == short.class) {
                return block.append(OpCode.D2I).append(OpCode.I2S);
            }
            if (targetType == int.class) {
                return block.append(OpCode.D2I);
            }
            if (targetType == long.class) {
                return block.append(OpCode.D2L);
            }
            if (targetType == float.class) {
                return block.append(OpCode.D2F);
            }
            if (targetType == double.class) {
                return block;
            }
        }
        throw new IllegalArgumentException(format("Type %s can not be cast to %s", sourceType, targetType));
    }

    private static TypeKind getTypeKind(ParameterizedType type)
    {
        if (type.isPrimitive()) {
            return TypeKind.PRIMITIVE;
        }
        if (unwrapPrimitiveType(type) != null) {
            return TypeKind.BOXED_PRIMITVE;
        }
        return TypeKind.OTHER;
    }

    private static Class<?> unwrapPrimitiveType(ParameterizedType boxedPrimitiveType)
    {
        switch (boxedPrimitiveType.getJavaClassName()) {
            case "java.lang.Boolean":
                return boolean.class;
            case "java.lang.Byte":
                return byte.class;
            case "java.lang.Character":
                return char.class;
            case "java.lang.Short":
                return short.class;
            case "java.lang.Integer":
                return int.class;
            case "java.lang.Long":
                return long.class;
            case "java.lang.Float":
                return float.class;
            case "java.lang.Double":
                return double.class;
            default:
                return null;
        }
    }

    @Override
    protected String formatOneLine()
    {
        return "((" + getType().getSimpleName() + ") " + instance + ")";
    }

    @Override
    public List<BytecodeNode> getChildNodes()
    {
        return ImmutableList.of(instance);
    }

    private enum TypeKind
    {
        PRIMITIVE, BOXED_PRIMITVE, OTHER
    }
}
