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

import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.BytecodeUtils;
import com.facebook.presto.bytecode.BytecodeVisitor;
import com.facebook.presto.bytecode.MethodGenerationContext;
import com.facebook.presto.bytecode.ParameterizedType;
import java.util.List;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import static com.facebook.presto.bytecode.OpCode.ACONST_NULL;
import static com.facebook.presto.bytecode.OpCode.BIPUSH;
import static com.facebook.presto.bytecode.OpCode.DCONST_0;
import static com.facebook.presto.bytecode.OpCode.DCONST_1;
import static com.facebook.presto.bytecode.OpCode.FCONST_0;
import static com.facebook.presto.bytecode.OpCode.FCONST_1;
import static com.facebook.presto.bytecode.OpCode.FCONST_2;
import static com.facebook.presto.bytecode.OpCode.ICONST_0;
import static com.facebook.presto.bytecode.OpCode.ICONST_1;
import static com.facebook.presto.bytecode.OpCode.ICONST_2;
import static com.facebook.presto.bytecode.OpCode.ICONST_3;
import static com.facebook.presto.bytecode.OpCode.ICONST_4;
import static com.facebook.presto.bytecode.OpCode.ICONST_5;
import static com.facebook.presto.bytecode.OpCode.ICONST_M1;
import static com.facebook.presto.bytecode.OpCode.LCONST_0;
import static com.facebook.presto.bytecode.OpCode.LCONST_1;
import static com.facebook.presto.bytecode.OpCode.SIPUSH;
import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.facebook.presto.bytecode.instruction.FieldInstruction.getStaticInstruction;
import static com.facebook.presto.bytecode.instruction.InvokeInstruction.invokeStatic;
import static java.util.Objects.requireNonNull;

@SuppressWarnings("UnusedDeclaration")
public abstract class Constant
    implements InstructionNode {
    public static Constant loadNull() {
        return new NullConstant();
    }

    public static Constant loadBoolean(boolean value) {
        return new BooleanConstant(value);
    }

    public static Constant loadBoxedBoolean(boolean value) {
        return new BoxedBooleanConstant(value);
    }

    public static Constant loadInt(int value) {
        return new IntConstant(value);
    }

    public static Constant loadBoxedInt(int value) {
        return new BoxedIntegerConstant(value);
    }

    public static Constant loadFloat(float value) {
        return new FloatConstant(value);
    }

    public static Constant loadBoxedFloat(float value) {
        return new BoxedFloatConstant(value);
    }

    public static Constant loadLong(long value) {
        return new LongConstant(value);
    }

    public static Constant loadBoxedLong(long value) {
        return new BoxedLongConstant(value);
    }

    public static Constant loadDouble(double value) {
        return new DoubleConstant(value);
    }

    public static Constant loadBoxedDouble(double value) {
        return new BoxedDoubleConstant(value);
    }

    public static Constant loadNumber(Number value) {
        requireNonNull(value, "value is null");
        if (value instanceof Byte) {
            return loadInt((value).intValue());
        }
        if (value instanceof Short) {
            return loadInt((value).intValue());
        }
        if (value instanceof Integer) {
            return loadInt((Integer)value);
        }
        if (value instanceof Long) {
            return loadLong((Long)value);
        }
        if (value instanceof Float) {
            return loadFloat((Float)value);
        }
        if (value instanceof Double) {
            return loadDouble((Double)value);
        }
        throw new IllegalStateException("Unsupported number type " + value.getClass().getSimpleName());
    }

    public static Constant loadString(String value) {
        requireNonNull(value, "value is null");
        return new StringConstant(value);
    }

    public static Constant loadClass(Class<?> value) {
        requireNonNull(value, "value is null");
        return new ClassConstant(type(value));
    }

    public static Constant loadClass(ParameterizedType value) {
        requireNonNull(value, "value is null");
        return new ClassConstant(value);
    }

    public abstract Object getValue();

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

    @Override
    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
        return visitor.visitConstant(parent, this);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{value=" + getValue() + '}';
    }

    public static class NullConstant
        extends Constant {
        @Override
        public Object getValue() {
            return null;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            visitor.visitInsn(ACONST_NULL.getOpCode());
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitConstant(parent, this);
        }
    }

    public static class BooleanConstant
        extends Constant {
        private final boolean value;

        private BooleanConstant(boolean value) {
            this.value = value;
        }

        @Override
        public Boolean getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            if (value) {
                visitor.visitInsn(ICONST_1.getOpCode());
            }
            else {
                visitor.visitInsn(ICONST_0.getOpCode());
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBooleanConstant(parent, this);
        }
    }

    public static class BoxedBooleanConstant
        extends Constant {
        private final boolean value;

        private BoxedBooleanConstant(boolean value) {
            this.value = value;
        }

        @Override
        public Boolean getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            if (value) {
                getStaticInstruction(Boolean.class, "TRUE", Boolean.class).accept(visitor, generationContext);
            }
            else {
                getStaticInstruction(Boolean.class, "FALSE", Boolean.class).accept(visitor, generationContext);
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBoxedBooleanConstant(parent, this);
        }
    }

    public static class IntConstant
        extends Constant {
        private final int value;

        private IntConstant(int value) {
            this.value = value;
        }

        @Override
        public Integer getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            if (value <= Byte.MAX_VALUE && value >= Byte.MIN_VALUE) {
                switch (value) {
                    case -1:
                        visitor.visitInsn(ICONST_M1.getOpCode());
                        break;
                    case 0:
                        visitor.visitInsn(ICONST_0.getOpCode());
                        break;
                    case 1:
                        visitor.visitInsn(ICONST_1.getOpCode());
                        break;
                    case 2:
                        visitor.visitInsn(ICONST_2.getOpCode());
                        break;
                    case 3:
                        visitor.visitInsn(ICONST_3.getOpCode());
                        break;
                    case 4:
                        visitor.visitInsn(ICONST_4.getOpCode());
                        break;
                    case 5:
                        visitor.visitInsn(ICONST_5.getOpCode());
                        break;
                    default:
                        visitor.visitIntInsn(BIPUSH.getOpCode(), value);
                        break;
                }
            }
            else if (value <= Short.MAX_VALUE && value >= Short.MIN_VALUE) {
                visitor.visitIntInsn(SIPUSH.getOpCode(), value);
            }
            else {
                visitor.visitLdcInsn(value);
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitIntConstant(parent, this);
        }
    }

    public static class BoxedIntegerConstant
        extends Constant {
        private final int value;

        private BoxedIntegerConstant(int value) {
            this.value = value;
        }

        @Override
        public Integer getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            loadInt(value).accept(visitor, generationContext);
            invokeStatic(Integer.class, "valueOf", Integer.class, int.class).accept(visitor, generationContext);
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBoxedIntegerConstant(parent, this);
        }
    }

    public static class FloatConstant
        extends Constant {
        private final float value;

        private FloatConstant(float value) {
            this.value = value;
        }

        @Override
        public Float getValue() {
            return value;
        }

        @Override
        @SuppressWarnings("FloatingPointEquality")
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            // We can not use "value == 0.0" because when value is "-0.0" the expression
            // will evaluate to true and we would convert "-0.0" to "0.0" which is
            // not the same value
            if (Float.floatToIntBits(value) == Float.floatToIntBits(0.0f)) {
                visitor.visitInsn(FCONST_0.getOpCode());
            }
            else if (value == 1.0f) {
                visitor.visitInsn(FCONST_1.getOpCode());
            }
            else if (value == 2.0f) {
                visitor.visitInsn(FCONST_2.getOpCode());
            }
            else {
                visitor.visitLdcInsn(value);
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitFloatConstant(parent, this);
        }
    }

    public static class BoxedFloatConstant
        extends Constant {
        private final float value;

        private BoxedFloatConstant(float value) {
            this.value = value;
        }

        @Override
        public Float getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            loadFloat(value).accept(visitor, generationContext);
            invokeStatic(Float.class, "valueOf", Float.class, float.class).accept(visitor, generationContext);
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBoxedFloatConstant(parent, this);
        }
    }

    public static class LongConstant
        extends Constant {
        private final long value;

        private LongConstant(long value) {
            this.value = value;
        }

        @Override
        public Long getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            if (value == 0) {
                visitor.visitInsn(LCONST_0.getOpCode());
            }
            else if (value == 1) {
                visitor.visitInsn(LCONST_1.getOpCode());
            }
            else {
                visitor.visitLdcInsn(value);
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitLongConstant(parent, this);
        }
    }

    public static class BoxedLongConstant
        extends Constant {
        private final long value;

        private BoxedLongConstant(long value) {
            this.value = value;
        }

        @Override
        public Long getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            loadLong(value).accept(visitor, generationContext);
            invokeStatic(Long.class, "valueOf", Long.class, long.class).accept(visitor, generationContext);
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBoxedLongConstant(parent, this);
        }
    }

    public static class DoubleConstant
        extends Constant {
        private final double value;

        private DoubleConstant(double value) {
            this.value = value;
        }

        @Override
        public Double getValue() {
            return value;
        }

        @Override
        @SuppressWarnings("FloatingPointEquality")
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            // We can not use "value == 0.0" because when value is "-0.0" the expression
            // will evaluate to true and we would convert "-0.0" to "0.0" which is
            // not the same value
            if (Double.doubleToLongBits(value) == Double.doubleToLongBits(0.0)) {
                visitor.visitInsn(DCONST_0.getOpCode());
            }
            else if (value == 1.0) {
                visitor.visitInsn(DCONST_1.getOpCode());
            }
            else {
                visitor.visitLdcInsn(value);
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitDoubleConstant(parent, this);
        }
    }

    public static class BoxedDoubleConstant
        extends Constant {
        private final double value;

        private BoxedDoubleConstant(double value) {
            this.value = value;
        }

        @Override
        public Double getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            loadDouble(value).accept(visitor, generationContext);
            invokeStatic(Double.class, "valueOf", Double.class, double.class).accept(visitor, generationContext);
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitBoxedDoubleConstant(parent, this);
        }
    }

    public static class StringConstant
        extends Constant {
        private final String value;

        private StringConstant(String value) {
            this.value = value;
        }

        @Override
        public String getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            visitor.visitLdcInsn(value);
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitStringConstant(parent, this);
        }
    }

    public static class ClassConstant
        extends Constant {
        private final ParameterizedType value;

        private ClassConstant(ParameterizedType value) {
            this.value = value;
        }

        @Override
        public ParameterizedType getValue() {
            return value;
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
            if (value.isPrimitive()) {
                Class<?> wrapper = BytecodeUtils.wrap(value.getPrimitiveType());
                getStaticInstruction(wrapper, "TYPE", Class.class).accept(visitor, generationContext);
            }
            else {
                visitor.visitLdcInsn(Type.getType(value.getType()));
            }
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
            return visitor.visitClassConstant(parent, this);
        }
    }
}
