// 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.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
    /// <summary>
    /// An error type, used to represent the type of a type binding
    /// operation when binding fails.
    /// </summary>
    internal sealed class ExtendedErrorTypeSymbol : ErrorTypeSymbol
    {
        private readonly string _name;
        private readonly int _arity;
        private readonly DiagnosticInfo? _errorInfo;
        private readonly NamespaceOrTypeSymbol? _containingSymbol;
        private readonly bool _unreported;
        public readonly bool VariableUsedBeforeDeclaration;
        private readonly ImmutableArray<Symbol> _candidateSymbols;  // Best guess at what user meant, but was wrong.
        private readonly LookupResultKind _resultKind; // why the guessSymbols were wrong.

        internal ExtendedErrorTypeSymbol(CSharpCompilation compilation, string name, int arity, DiagnosticInfo? errorInfo, bool unreported = false, bool variableUsedBeforeDeclaration = false)
            : this(compilation.Assembly.GlobalNamespace, name, arity, errorInfo, unreported, variableUsedBeforeDeclaration)
        {
        }

        internal ExtendedErrorTypeSymbol(NamespaceOrTypeSymbol? containingSymbol, string name, int arity, DiagnosticInfo? errorInfo, bool unreported = false, bool variableUsedBeforeDeclaration = false)
        {
            Debug.Assert(((object?)containingSymbol == null) ||
                (containingSymbol.Kind == SymbolKind.Namespace) ||
                (containingSymbol.Kind == SymbolKind.NamedType) ||
                (containingSymbol.Kind == SymbolKind.ErrorType));

            RoslynDebug.Assert(name != null);
            Debug.Assert(unreported == false || errorInfo != null);

            _name = name;
            _errorInfo = errorInfo;
            _containingSymbol = containingSymbol;
            _arity = arity;
            _unreported = unreported;
            this.VariableUsedBeforeDeclaration = variableUsedBeforeDeclaration;
            _resultKind = LookupResultKind.Empty;
        }

        private ExtendedErrorTypeSymbol(NamespaceOrTypeSymbol? containingSymbol, string name, int arity, DiagnosticInfo? errorInfo, bool unreported, bool variableUsedBeforeDeclaration, ImmutableArray<Symbol> candidateSymbols, LookupResultKind resultKind)
        {
            _name = name;
            _errorInfo = errorInfo;
            _containingSymbol = containingSymbol;
            _arity = arity;
            _unreported = unreported;
            this.VariableUsedBeforeDeclaration = variableUsedBeforeDeclaration;
            _candidateSymbols = candidateSymbols;
            _resultKind = resultKind;
        }

        internal ExtendedErrorTypeSymbol(NamespaceOrTypeSymbol guessSymbol, LookupResultKind resultKind, DiagnosticInfo errorInfo, bool unreported = false)
            : this(guessSymbol.ContainingNamespaceOrType(), guessSymbol, resultKind, errorInfo, unreported)
        {
        }

        internal ExtendedErrorTypeSymbol(NamespaceOrTypeSymbol? containingSymbol, Symbol guessSymbol, LookupResultKind resultKind, DiagnosticInfo errorInfo, bool unreported = false)
            : this(containingSymbol, ImmutableArray.Create<Symbol>(guessSymbol), resultKind, errorInfo, GetArity(guessSymbol), unreported)
        {
        }

        internal ExtendedErrorTypeSymbol(NamespaceOrTypeSymbol? containingSymbol, ImmutableArray<Symbol> candidateSymbols, LookupResultKind resultKind, DiagnosticInfo errorInfo, int arity, bool unreported = false)
            : this(containingSymbol, candidateSymbols[0].Name, arity, errorInfo, unreported)
        {
            _candidateSymbols = UnwrapErrorCandidates(candidateSymbols);
            _resultKind = resultKind;
            Debug.Assert(candidateSymbols.IsEmpty || resultKind != LookupResultKind.Viable, "Shouldn't use LookupResultKind.Viable with candidate symbols");
        }

        internal ExtendedErrorTypeSymbol AsUnreported()
        {
            return this.Unreported ? this :
                new ExtendedErrorTypeSymbol(_containingSymbol, _name, _arity, _errorInfo, true, VariableUsedBeforeDeclaration, _candidateSymbols, _resultKind);
        }

        private static ImmutableArray<Symbol> UnwrapErrorCandidates(ImmutableArray<Symbol> candidateSymbols)
        {
            var candidate = candidateSymbols.IsEmpty ? null : candidateSymbols[0] as ErrorTypeSymbol;
            return ((object?)candidate != null && !candidate.CandidateSymbols.IsEmpty) ? candidate.CandidateSymbols : candidateSymbols;
        }

        protected override NamedTypeSymbol WithTupleDataCore(TupleExtraData newData)
        {
            throw ExceptionUtilities.Unreachable;
        }

        internal override DiagnosticInfo? ErrorInfo
        {
            get
            {
                return _errorInfo;
            }
        }

        internal override LookupResultKind ResultKind
        {
            get
            {
                return _resultKind;
            }
        }

        public override ImmutableArray<Symbol> CandidateSymbols => _candidateSymbols.NullToEmpty();

        internal override bool Unreported
        {
            get { return _unreported; }
        }

        internal override DiagnosticInfo? GetUseSiteDiagnostic()
        {
            if (_unreported)
            {
                return this.ErrorInfo;
            }

            return null;
        }

        public override int Arity
        {
            get
            {
                return _arity;
            }
        }

        internal override bool MangleName
        {
            get
            {
                return _arity > 0;
            }
        }

        public override Symbol? ContainingSymbol
        {
            get
            {
                return _containingSymbol;
            }
        }

        public override string Name
        {
            get
            {
                return _name;
            }
        }

        public override NamedTypeSymbol OriginalDefinition
        {
            get
            {
                return this;
            }
        }

        // public override SymbolKind Kind { get { return SymbolKind.Error; } }
        public override ImmutableArray<Location> Locations
        {
            get
            {
                return ImmutableArray<Location>.Empty;
            }
        }

        public override NamedTypeSymbol ConstructedFrom
        {
            get
            {
                return this;
            }
        }

        internal override NamedTypeSymbol? GetDeclaredBaseType(ConsList<TypeSymbol> basesBeingResolved)
        {
            return null;
        }

        internal override ImmutableArray<NamedTypeSymbol> GetDeclaredInterfaces(ConsList<TypeSymbol> basesBeingResolved)
        {
            return ImmutableArray<NamedTypeSymbol>.Empty;
        }

        /// <summary>
        /// If (we believe) we know which symbol the user intended, then we should retain that information
        /// in the corresponding error symbol - it can be useful for deciding how to handle the error.
        /// For example, we might want to know whether (we believe) the error type was supposed to be an
        /// interface, so that we can put it in a derived type's interface list, rather than in the base
        /// type slot.
        /// 
        /// Sometimes we will return the original definition of the intended symbol.  For example, if we see 
        /// <![CDATA[IGoo<int>]]> and we have an IGoo with a different arity or accessibility 
        /// (e.g. <![CDATA[IGoo<int>]]> was constructed from an error symbol based on <![CDATA[IGoo<T>]]>), 
        /// then we'll return <![CDATA[IGoo<T>]]>, rather than trying to construct a corresponding closed
        /// type (which may not be difficult/possible in the case of nested types or mismatched arities).
        /// 
        /// NOTE: Any non-null type symbol returned is guaranteed not to be an error type.
        /// </summary>
        /// <remarks>
        /// TypeSymbolExtensions.GetNonErrorGuess is a more discoverable version of this functionality.
        /// However, the real definition is in this class so that it can access the private field 
        /// nonErrorGuessType.
        /// </remarks>
        internal static TypeSymbol? ExtractNonErrorType(TypeSymbol? oldSymbol)
        {
            if ((object?)oldSymbol == null || oldSymbol.TypeKind != TypeKind.Error)
            {
                return oldSymbol;
            }

            // At this point, we know that oldSymbol is a non-null type symbol with kind error.
            // Hence, it is either an ErrorTypeSymbol or it has an ErrorTypeSymbol as its
            // original definition.  In the former case, it is its own original definition.
            // Thus, if there's a CSErrorTypeSymbol in there somewhere, it's returned by
            // OriginalDefinition.
            ExtendedErrorTypeSymbol? oldError = oldSymbol.OriginalDefinition as ExtendedErrorTypeSymbol;

            // If the original definition isn't a CSErrorTypeSymbol, then we don't know how to
            // pull out a non-error type.  If it is, then if there is a unambiguous type inside it,
            // use that.
            if ((object?)oldError != null && !oldError._candidateSymbols.IsDefault && oldError._candidateSymbols.Length == 1)
            {
                TypeSymbol? type = oldError._candidateSymbols[0] as TypeSymbol;
                if ((object?)type != null)
                    return type.GetNonErrorGuess();
            }

            return null;
        }

        // Get the type kind of a symbol, going to candidates if possible.
        internal static TypeKind ExtractNonErrorTypeKind(TypeSymbol oldSymbol)
        {
            if (oldSymbol.TypeKind != TypeKind.Error)
            {
                return oldSymbol.TypeKind;
            }

            // At this point, we know that oldSymbol is a non-null type symbol with kind error.
            // Hence, it is either an ErrorTypeSymbol or it has an ErrorTypeSymbol as its
            // original definition.  In the former case, it is its own original definition.
            // Thus, if there's a CSErrorTypeSymbol in there somewhere, it's returned by
            // OriginalDefinition.
            ExtendedErrorTypeSymbol? oldError = oldSymbol.OriginalDefinition as ExtendedErrorTypeSymbol;

            // If the original definition isn't a CSErrorTypeSymbol, then we don't know how to
            // pull out a non-error type.  If it is, then if there is a unambiguous type inside it,
            // use that.
            TypeKind commonTypeKind = TypeKind.Error;
            if ((object?)oldError != null && !oldError._candidateSymbols.IsDefault && oldError._candidateSymbols.Length > 0)
            {
                foreach (Symbol sym in oldError._candidateSymbols)
                {
                    TypeSymbol? type = sym as TypeSymbol;
                    if ((object?)type != null && type.TypeKind != TypeKind.Error)
                    {
                        if (commonTypeKind == TypeKind.Error)
                            commonTypeKind = type.TypeKind;
                        else if (commonTypeKind != type.TypeKind)
                            return TypeKind.Error;  // no common kind.
                    }
                }
            }

            return commonTypeKind;
        }

        internal override bool Equals(TypeSymbol? t2, TypeCompareKind comparison)
        {
            if (ReferenceEquals(this, t2))
            {
                return true;
            }

            var other = t2 as ExtendedErrorTypeSymbol;
            if ((object?)other == null || _unreported || other._unreported)
            {
                return false;
            }

            return
                ((object)this.ContainingType != null ? this.ContainingType.Equals(other.ContainingType, comparison) :
                 (object?)this.ContainingSymbol == null ? (object?)other.ContainingSymbol == null : this.ContainingSymbol.Equals(other.ContainingSymbol)) &&
                this.Name == other.Name && this.Arity == other.Arity;
        }

        public override int GetHashCode()
        {
            return Hash.Combine(this.Arity,
                        Hash.Combine((object?)this.ContainingSymbol != null ? this.ContainingSymbol.GetHashCode() : 0,
                                     this.Name != null ? this.Name.GetHashCode() : 0));
        }

        private static int GetArity(Symbol symbol)
        {
            switch (symbol.Kind)
            {
                case SymbolKind.NamedType:
                case SymbolKind.ErrorType:
                    return ((NamedTypeSymbol)symbol).Arity;
                case SymbolKind.Method:
                    return ((MethodSymbol)symbol).Arity;
                default:
                    return 0;
            }
        }
    }
}
