// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Dynamic.Utils;

namespace System.Linq.Expressions.Interpreter
{
    internal abstract class NumericConvertInstruction : Instruction
    {
        internal readonly TypeCode _from, _to;
        private readonly bool _isLiftedToNull;

        protected NumericConvertInstruction(TypeCode from, TypeCode to, bool isLiftedToNull)
        {
            _from = from;
            _to = to;
            _isLiftedToNull = isLiftedToNull;
        }

        public sealed override int Run(InterpretedFrame frame)
        {
            object? obj = frame.Pop();
            object? converted;
            if (obj == null)
            {
                if (_isLiftedToNull)
                {
                    converted = null;
                }
                else
                {
                    // We cannot have null in a non-lifted numeric context. Throw the exception
                    // about not Nullable object requiring a value.
                    return (int)(int?)obj!;
                }
            }
            else
            {
                converted = Convert(obj);
            }

            frame.Push(converted);
            return 1;
        }

        protected abstract object Convert(object obj);

        public override string InstructionName => "NumericConvert";
        public override int ConsumedStack => 1;
        public override int ProducedStack => 1;

        public override string ToString() => InstructionName + "(" + _from + "->" + _to + ")";

        internal sealed class Unchecked : NumericConvertInstruction
        {
            public override string InstructionName => "UncheckedConvert";

            public Unchecked(TypeCode from, TypeCode to, bool isLiftedToNull)
                : base(from, to, isLiftedToNull)
            {
            }

            protected override object Convert(object obj)
            {
                return _from switch
                {
                    TypeCode.Boolean => ConvertInt32((bool)obj ? 1 : 0),
                    TypeCode.Byte => ConvertInt32((byte)obj),
                    TypeCode.SByte => ConvertInt32((sbyte)obj),
                    TypeCode.Int16 => ConvertInt32((short)obj),
                    TypeCode.Char => ConvertInt32((char)obj),
                    TypeCode.Int32 => ConvertInt32((int)obj),
                    TypeCode.Int64 => ConvertInt64((long)obj),
                    TypeCode.UInt16 => ConvertInt32((ushort)obj),
                    TypeCode.UInt32 => ConvertInt64((uint)obj),
                    TypeCode.UInt64 => ConvertUInt64((ulong)obj),
                    TypeCode.Single => ConvertDouble((float)obj),
                    TypeCode.Double => ConvertDouble((double)obj),
                    _ => throw ContractUtils.Unreachable,
                };
            }

            private object ConvertInt32(int obj)
            {
                unchecked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        TypeCode.Boolean => obj != 0,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertInt64(long obj)
            {
                unchecked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertUInt64(ulong obj)
            {
                unchecked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertDouble(double obj)
            {
                unchecked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }
        }

        internal sealed class Checked : NumericConvertInstruction
        {
            public override string InstructionName => "CheckedConvert";

            public Checked(TypeCode from, TypeCode to, bool isLiftedToNull)
                : base(from, to, isLiftedToNull)
            {
            }

            protected override object Convert(object obj)
            {
                return _from switch
                {
                    TypeCode.Boolean => ConvertInt32((bool)obj ? 1 : 0),
                    TypeCode.Byte => ConvertInt32((byte)obj),
                    TypeCode.SByte => ConvertInt32((sbyte)obj),
                    TypeCode.Int16 => ConvertInt32((short)obj),
                    TypeCode.Char => ConvertInt32((char)obj),
                    TypeCode.Int32 => ConvertInt32((int)obj),
                    TypeCode.Int64 => ConvertInt64((long)obj),
                    TypeCode.UInt16 => ConvertInt32((ushort)obj),
                    TypeCode.UInt32 => ConvertInt64((uint)obj),
                    TypeCode.UInt64 => ConvertUInt64((ulong)obj),
                    TypeCode.Single => ConvertDouble((float)obj),
                    TypeCode.Double => ConvertDouble((double)obj),
                    _ => throw ContractUtils.Unreachable,
                };
            }

            private object ConvertInt32(int obj)
            {
                checked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        TypeCode.Boolean => obj != 0,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertInt64(long obj)
            {
                checked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertUInt64(ulong obj)
            {
                checked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }

            private object ConvertDouble(double obj)
            {
                checked
                {
                    return _to switch
                    {
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        TypeCode.Single => (float)obj,
                        TypeCode.Double => (double)obj,
                        TypeCode.Decimal => (decimal)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }
        }

        internal sealed class ToUnderlying : NumericConvertInstruction
        {
            public override string InstructionName => "ConvertToUnderlying";

            public ToUnderlying(TypeCode to, bool isLiftedToNull)
                : base(to, to, isLiftedToNull)
            {
            }

            protected override object Convert(object obj)
            {
                unchecked
                {
                    return _to switch
                    {
                        TypeCode.Boolean => (bool)obj,
                        TypeCode.Byte => (byte)obj,
                        TypeCode.SByte => (sbyte)obj,
                        TypeCode.Int16 => (short)obj,
                        TypeCode.Char => (char)obj,
                        TypeCode.Int32 => (int)obj,
                        TypeCode.Int64 => (long)obj,
                        TypeCode.UInt16 => (ushort)obj,
                        TypeCode.UInt32 => (uint)obj,
                        TypeCode.UInt64 => (ulong)obj,
                        _ => throw ContractUtils.Unreachable,
                    };
                }
            }
        }
    }
}
