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

using System;
using System.Globalization;

namespace Microsoft.CSharp.RuntimeBinder.Semantics
{
    /*
     * The kind of allocation used in a constant value.
     * Since CONSTVALs don't store a discriminant for the union
     * this must be managed by clients.
     */
    internal enum ConstValKind
    {
        Int,
        Double,
        Long,
        String,
        Decimal,
        IntPtr,
        Float,
        Boolean
    }

    internal readonly struct ConstVal
    {
        // Pre-boxed common values.
        private static readonly object s_false = false;
        private static readonly object s_true = true;
        private static readonly object s_zeroInt32 = 0;

        private ConstVal(object value)
        {
            ObjectVal = value;
        }

        public object ObjectVal { get; }

        public bool BooleanVal => SpecialUnbox<bool>(ObjectVal);

        public sbyte SByteVal => SpecialUnbox<sbyte>(ObjectVal);

        public byte ByteVal => SpecialUnbox<byte>(ObjectVal);

        public short Int16Val => SpecialUnbox<short>(ObjectVal);

        public ushort UInt16Val => SpecialUnbox<ushort>(ObjectVal);

        public int Int32Val => SpecialUnbox<int>(ObjectVal);

        public uint UInt32Val => SpecialUnbox<uint>(ObjectVal);

        public long Int64Val => SpecialUnbox<long>(ObjectVal);

        public ulong UInt64Val => SpecialUnbox<ulong>(ObjectVal);

        public float SingleVal => SpecialUnbox<float>(ObjectVal);

        public double DoubleVal => SpecialUnbox<double>(ObjectVal);

        public decimal DecimalVal => SpecialUnbox<decimal>(ObjectVal);

        public char CharVal => SpecialUnbox<char>(ObjectVal);

        public string StringVal => SpecialUnbox<string>(ObjectVal);

        public bool IsNullRef => ObjectVal == null;

        public bool IsZero(ConstValKind kind) =>
            kind switch
            {
                ConstValKind.Decimal => DecimalVal == 0,
                ConstValKind.String => false,
                _ => IsDefault(ObjectVal),
            };

        private static T SpecialUnbox<T>(object o)
        {
            if (IsDefault(o))
            {
                return default(T);
            }

            return (T)Convert.ChangeType(o, typeof(T), CultureInfo.InvariantCulture);
        }

        private static bool IsDefault(object o) =>
            o is null ||
            Type.GetTypeCode(o.GetType()) switch
            {
                TypeCode.Boolean => default(bool).Equals(o),
                TypeCode.SByte => default(sbyte).Equals(o),
                TypeCode.Byte => default(byte).Equals(o),
                TypeCode.Int16 => default(short).Equals(o),
                TypeCode.UInt16 => default(ushort).Equals(o),
                TypeCode.Int32 => default(int).Equals(o),
                TypeCode.UInt32 => default(uint).Equals(o),
                TypeCode.Int64 => default(long).Equals(o),
                TypeCode.UInt64 => default(ulong).Equals(o),
                TypeCode.Single => default(float).Equals(o),
                TypeCode.Double => default(double).Equals(o),
                TypeCode.Decimal => default(decimal).Equals(o),
                TypeCode.Char => default(char).Equals(o),

                _ => false,
            };

        public static ConstVal GetDefaultValue(ConstValKind kind) =>
            kind switch
            {
                ConstValKind.Int => new ConstVal(s_zeroInt32),
                ConstValKind.Double => new ConstVal(0.0),
                ConstValKind.Long => new ConstVal(0L),
                ConstValKind.Decimal => new ConstVal(0M),
                ConstValKind.Float => new ConstVal(0F),
                ConstValKind.Boolean => new ConstVal(s_false),
                _ => default,
            };

        public static ConstVal Get(bool value) => new ConstVal(value ? s_true : s_false);

        public static ConstVal Get(int value) => new ConstVal(value == 0 ? s_zeroInt32 : value);

        public static ConstVal Get(uint value) => new ConstVal(value);

        public static ConstVal Get(decimal value) => new ConstVal(value);

        public static ConstVal Get(string value) => new ConstVal(value);

        public static ConstVal Get(float value) => new ConstVal(value);

        public static ConstVal Get(double value) => new ConstVal(value);

        public static ConstVal Get(long value) => new ConstVal(value);

        public static ConstVal Get(ulong value) => new ConstVal(value);

        public static ConstVal Get(object p) => new ConstVal(p);
    }
}
