// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
    [Flags]
    internal enum TypeParameterConstraintKind
    {
        None = 0x00,
        ReferenceType = 0x01,
        ValueType = 0x02,
        Constructor = 0x04,
        Unmanaged = 0x08,
        NullableReferenceType = ReferenceType | 0x10,
        NotNullableReferenceType = ReferenceType | 0x20,

        /// <summary>
        /// Type parameter has no type constraints, including `struct`, `class`, `unmanaged` and is declared in a context
        /// where nullable annotations are disabled.
        /// Cannot be combined with <see cref="ReferenceType"/>, <see cref="ValueType"/> or <see cref="Unmanaged"/>.
        /// Note, presence of this flag suppresses generation of Nullable attribute on the corresponding type parameter.
        /// This imitates the shape of metadata produced by pre-nullable compilers. Metadata import is adjusted accordingly
        /// to distinguish between the two situations.
        /// </summary>
        ObliviousNullabilityIfReferenceType = 0x40,

        NotNull = 0x80,
        Default = 0x100,

        /// <summary>
        /// <see cref="TypeParameterConstraintKind"/> mismatch is detected during merging process for partial type declarations.
        /// </summary>
        PartialMismatch = 0x200,
        ValueTypeFromConstraintTypes = 0x400, // Not set if any flag from AllValueTypeKinds is set
        ReferenceTypeFromConstraintTypes = 0x800,

        AllowByRefLike = 0x1000,

        /// <summary>
        /// All bits involved into describing various aspects of 'class' constraint.
        /// </summary>
        AllReferenceTypeKinds = NullableReferenceType | NotNullableReferenceType,

        /// <summary>
        /// Any of these bits is equivalent to presence of 'struct' constraint.
        /// </summary>
        AllValueTypeKinds = ValueType | Unmanaged,

        /// <summary>
        /// All bits except those that are involved into describing various nullability aspects.
        /// </summary>
        AllNonNullableKinds = ReferenceType | ValueType | Constructor | Unmanaged | AllowByRefLike,
    }

    /// <summary>
    /// A simple representation of a type parameter constraint clause
    /// as a set of constraint bits and a set of constraint types.
    /// </summary>
    internal sealed class TypeParameterConstraintClause
    {
        internal static readonly TypeParameterConstraintClause Empty = new TypeParameterConstraintClause(
            TypeParameterConstraintKind.None,
            ImmutableArray<TypeWithAnnotations>.Empty);

        internal static readonly TypeParameterConstraintClause ObliviousNullabilityIfReferenceType = new TypeParameterConstraintClause(
            TypeParameterConstraintKind.ObliviousNullabilityIfReferenceType,
            ImmutableArray<TypeWithAnnotations>.Empty);

        internal static TypeParameterConstraintClause Create(
            TypeParameterConstraintKind constraints,
            ImmutableArray<TypeWithAnnotations> constraintTypes)
        {
            Debug.Assert(!constraintTypes.IsDefault);
            if (constraintTypes.IsEmpty)
            {
                switch (constraints)
                {
                    case TypeParameterConstraintKind.None:
                        return Empty;

                    case TypeParameterConstraintKind.ObliviousNullabilityIfReferenceType:
                        return ObliviousNullabilityIfReferenceType;
                }
            }

            return new TypeParameterConstraintClause(constraints, constraintTypes);
        }

        private TypeParameterConstraintClause(
            TypeParameterConstraintKind constraints,
            ImmutableArray<TypeWithAnnotations> constraintTypes)
        {
#if DEBUG
            switch (constraints & TypeParameterConstraintKind.AllReferenceTypeKinds)
            {
                case TypeParameterConstraintKind.None:
                case TypeParameterConstraintKind.ReferenceType:
                case TypeParameterConstraintKind.NullableReferenceType:
                case TypeParameterConstraintKind.NotNullableReferenceType:
                    break;
                default:
                    ExceptionUtilities.UnexpectedValue(constraints); // This call asserts.
                    break;
            }

            Debug.Assert((constraints & TypeParameterConstraintKind.ObliviousNullabilityIfReferenceType) == 0 ||
                         (constraints & ~(TypeParameterConstraintKind.ObliviousNullabilityIfReferenceType | TypeParameterConstraintKind.Constructor |
                                          TypeParameterConstraintKind.Default | TypeParameterConstraintKind.PartialMismatch |
                                          TypeParameterConstraintKind.ValueTypeFromConstraintTypes | TypeParameterConstraintKind.ReferenceTypeFromConstraintTypes |
                                          TypeParameterConstraintKind.AllowByRefLike)) == 0);
#endif
            this.Constraints = constraints;
            this.ConstraintTypes = constraintTypes;
        }

        public readonly TypeParameterConstraintKind Constraints;
        public readonly ImmutableArray<TypeWithAnnotations> ConstraintTypes;

        internal static SmallDictionary<TypeParameterSymbol, bool> BuildIsValueTypeMap(
            ImmutableArray<TypeParameterSymbol> typeParameters,
            ImmutableArray<TypeParameterConstraintClause> constraintClauses)
        {
            Debug.Assert(constraintClauses.Length == typeParameters.Length);

            var isValueTypeMap = new SmallDictionary<TypeParameterSymbol, bool>(ReferenceEqualityComparer.Instance);

            foreach (TypeParameterSymbol typeParameter in typeParameters)
            {
                isValueType(typeParameter, constraintClauses, isValueTypeMap, ConsList<TypeParameterSymbol>.Empty);
            }

            return isValueTypeMap;

            static bool isValueType(TypeParameterSymbol thisTypeParameter, ImmutableArray<TypeParameterConstraintClause> constraintClauses, SmallDictionary<TypeParameterSymbol, bool> isValueTypeMap, ConsList<TypeParameterSymbol> inProgress)
            {
                if (inProgress.ContainsReference(thisTypeParameter))
                {
                    return false;
                }

                if (isValueTypeMap.TryGetValue(thisTypeParameter, out bool knownIsValueType))
                {
                    return knownIsValueType;
                }

                TypeParameterConstraintClause constraintClause = constraintClauses[thisTypeParameter.Ordinal];

                bool result = false;

                if ((constraintClause.Constraints & TypeParameterConstraintKind.AllValueTypeKinds) != 0)
                {
                    result = true;
                }
                else
                {
                    Symbol container = thisTypeParameter.ContainingSymbol;
                    inProgress = inProgress.Prepend(thisTypeParameter);

                    foreach (TypeWithAnnotations constraintType in constraintClause.ConstraintTypes)
                    {
                        TypeSymbol type = constraintType.IsResolved ? constraintType.Type : constraintType.DefaultType;

                        if (type is TypeParameterSymbol typeParameter && (object)typeParameter.ContainingSymbol == (object)container)
                        {
                            if (isValueType(typeParameter, constraintClauses, isValueTypeMap, inProgress))
                            {
                                result = true;
                                break;
                            }
                        }
                        else if (type.IsValueType)
                        {
                            result = true;
                            break;
                        }
                    }
                }

                isValueTypeMap.Add(thisTypeParameter, result);
                return result;
            }
        }

        internal static SmallDictionary<TypeParameterSymbol, bool> BuildIsReferenceTypeFromConstraintTypesMap(
            ImmutableArray<TypeParameterSymbol> typeParameters,
            ImmutableArray<TypeParameterConstraintClause> constraintClauses)
        {
            Debug.Assert(constraintClauses.Length == typeParameters.Length);

            var isReferenceTypeFromConstraintTypesMap = new SmallDictionary<TypeParameterSymbol, bool>(ReferenceEqualityComparer.Instance);

            foreach (TypeParameterSymbol typeParameter in typeParameters)
            {
                isReferenceTypeFromConstraintTypes(typeParameter, constraintClauses, isReferenceTypeFromConstraintTypesMap, ConsList<TypeParameterSymbol>.Empty);
            }

            return isReferenceTypeFromConstraintTypesMap;

            static bool isReferenceTypeFromConstraintTypes(TypeParameterSymbol thisTypeParameter, ImmutableArray<TypeParameterConstraintClause> constraintClauses,
                                                           SmallDictionary<TypeParameterSymbol, bool> isReferenceTypeFromConstraintTypesMap, ConsList<TypeParameterSymbol> inProgress)
            {
                if (inProgress.ContainsReference(thisTypeParameter))
                {
                    return false;
                }

                if (isReferenceTypeFromConstraintTypesMap.TryGetValue(thisTypeParameter, out bool knownIsReferenceTypeFromConstraintTypes))
                {
                    return knownIsReferenceTypeFromConstraintTypes;
                }

                TypeParameterConstraintClause constraintClause = constraintClauses[thisTypeParameter.Ordinal];

                bool result = false;

                Symbol container = thisTypeParameter.ContainingSymbol;
                inProgress = inProgress.Prepend(thisTypeParameter);

                foreach (TypeWithAnnotations constraintType in constraintClause.ConstraintTypes)
                {
                    TypeSymbol type = constraintType.IsResolved ? constraintType.Type : constraintType.DefaultType;

                    if (type is TypeParameterSymbol typeParameter)
                    {
                        if ((object)typeParameter.ContainingSymbol == (object)container)
                        {
                            if (isReferenceTypeFromConstraintTypes(typeParameter, constraintClauses, isReferenceTypeFromConstraintTypesMap, inProgress))
                            {
                                result = true;
                                break;
                            }
                        }
                        else if (typeParameter.IsReferenceTypeFromConstraintTypes)
                        {
                            result = true;
                            break;
                        }
                    }
                    else if (TypeParameterSymbol.NonTypeParameterConstraintImpliesReferenceType(type))
                    {
                        result = true;
                        break;
                    }
                }

                isReferenceTypeFromConstraintTypesMap.Add(thisTypeParameter, result);
                return result;
            }
        }
    }
}
