// 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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.SymbolId;

public abstract class SymbolKeyTestBase : CSharpTestBase
{
    [Flags]
    internal enum SymbolKeyComparison
    {
        None = 0x0,
        IgnoreCase = 0x1,
        IgnoreAssemblyIds = 0x2
    }

    [Flags]
    internal enum SymbolCategory
    {
        All = 0,
        DeclaredNamespace = 2,
        DeclaredType = 4,
        NonTypeMember = 8,
        Parameter = 16,
    }

    #region "Verification"

    internal static void ResolveAndVerifySymbolList(IEnumerable<ISymbol> newSymbols, IEnumerable<ISymbol> originalSymbols, CSharpCompilation originalComp)
    {
        var newlist = newSymbols.OrderBy(s => s.Name).ToList();
        var origlist = originalSymbols.OrderBy(s => s.Name).ToList();

        Assert.Equal(origlist.Count, newlist.Count);

        for (var i = 0; i < newlist.Count; i++)
        {
            ResolveAndVerifySymbol(newlist[i], origlist[i], originalComp);
        }
    }

    internal static void ResolveAndVerifyTypeSymbol(ExpressionSyntax node, ITypeSymbol sourceSymbol, SemanticModel model, CSharpCompilation sourceComp)
    {
        var typeinfo = model.GetTypeInfo(node);
        ResolveAndVerifySymbol(typeinfo.Type ?? typeinfo.ConvertedType, sourceSymbol, sourceComp);
    }

    internal static void ResolveAndVerifySymbol(ExpressionSyntax node, ISymbol sourceSymbol, SemanticModel model, CSharpCompilation sourceComp, SymbolKeyComparison comparison = SymbolKeyComparison.None)
    {
        var syminfo = model.GetSymbolInfo(node);
        ResolveAndVerifySymbol(syminfo.Symbol, sourceSymbol, sourceComp, comparison);
    }

    internal static void ResolveAndVerifySymbol(ISymbol symbol1, ISymbol symbol2, Compilation compilation2, SymbolKeyComparison comparison = SymbolKeyComparison.None)
    {
        // same ID
        AssertSymbolKeysEqual(symbol1, symbol2, comparison);

        var resolvedSymbol = ResolveSymbol(symbol1, compilation2, comparison);

        Assert.NotNull(resolvedSymbol);

        // same Symbol
        Assert.Equal(symbol2, resolvedSymbol);
        Assert.Equal(symbol2.GetHashCode(), resolvedSymbol.GetHashCode());
    }

    internal static ISymbol ResolveSymbol(ISymbol originalSymbol, Compilation targetCompilation, SymbolKeyComparison comparison)
    {
        var sid = SymbolKey.Create(originalSymbol, CancellationToken.None);

        // Verify that serialization works.
        var serialized = sid.ToString();
        var deserialized = new SymbolKey(serialized);
        var comparer = SymbolKey.GetComparer(ignoreCase: false, ignoreAssemblyKeys: false);
        Assert.True(comparer.Equals(sid, deserialized));

        var symInfo = sid.Resolve(targetCompilation, (comparison & SymbolKeyComparison.IgnoreAssemblyIds) == SymbolKeyComparison.IgnoreAssemblyIds);
        return symInfo.Symbol;
    }

    internal static void AssertSymbolKeysEqual(ISymbol symbol1, ISymbol symbol2, SymbolKeyComparison comparison, bool expectEqual = true)
    {
        var sid1 = SymbolKey.Create(symbol1, CancellationToken.None);
        var sid2 = SymbolKey.Create(symbol2, CancellationToken.None);

        // default is Insensitive
        var ignoreCase = (comparison & SymbolKeyComparison.IgnoreCase) == SymbolKeyComparison.IgnoreCase;

        // default is NOT ignore
        var ignoreAssemblyIds = (comparison & SymbolKeyComparison.IgnoreAssemblyIds) == SymbolKeyComparison.IgnoreAssemblyIds;
        var message = string.Concat(
            ignoreCase ? "SymbolID IgnoreCase" : "SymbolID",
            ignoreAssemblyIds ? " IgnoreAssemblyIds " : " ",
            "Compare");

        var ret = CodeAnalysis.SymbolKey.GetComparer(ignoreCase, ignoreAssemblyIds).Equals(sid2, sid1);
        if (expectEqual)
        {
            Assert.True(ret, message);
        }
        else
        {
            Assert.False(ret, message);
        }
    }

    #endregion

    #region "Utilities"

    internal static List<BlockSyntax> GetBlockSyntaxList(IMethodSymbol symbol)
    {
        var list = new List<BlockSyntax>();

        foreach (var node in symbol.DeclaringSyntaxReferences.Select(d => d.GetSyntax()))
        {
            BlockSyntax body = null;
            if (node is BaseMethodDeclarationSyntax baseMethod)
            {
                body = baseMethod.Body;
            }
            else if (node is AccessorDeclarationSyntax accessor)
            {
                body = accessor.Body;
            }

            if (body != null && body.Statements.Any())
            {
                list.Add(body);
            }
        }

        return list;
    }

    internal static IEnumerable<ISymbol> GetSourceSymbols(Microsoft.CodeAnalysis.CSharp.CSharpCompilation compilation, SymbolCategory category)
    {
        // NYI for local symbols
        var list = GetSourceSymbols(compilation, includeLocal: false);

        var kinds = new List<SymbolKind>();
        if ((category & SymbolCategory.DeclaredNamespace) != 0)
        {
            kinds.Add(SymbolKind.Namespace);
        }

        if ((category & SymbolCategory.DeclaredType) != 0)
        {
            kinds.Add(SymbolKind.NamedType);
            kinds.Add(SymbolKind.TypeParameter);
        }

        if ((category & SymbolCategory.NonTypeMember) != 0)
        {
            kinds.Add(SymbolKind.Field);
            kinds.Add(SymbolKind.Event);
            kinds.Add(SymbolKind.Property);
            kinds.Add(SymbolKind.Method);
        }

        if ((category & SymbolCategory.Parameter) != 0)
        {
            kinds.Add(SymbolKind.Parameter);
        }

        return list.Where(s =>
        {
            if (s.IsImplicitlyDeclared)
            {
                return false;
            }

            foreach (var k in kinds)
            {
                if (s.Kind == k)
                {
                    return true;
                }
            }

            return false;
        });
    }

    internal static IList<ISymbol> GetSourceSymbols(CSharpCompilation compilation, bool includeLocal)
    {
        var list = new List<ISymbol>();
        var localDumper = includeLocal ? new LocalSymbolDumper(compilation) : null;
        GetSourceMemberSymbols(compilation.SourceModule.GlobalNamespace.GetPublicSymbol(), list, localDumper);

        // ??
        // if (includeLocal)
        GetSourceAliasSymbols(compilation, list);
        Compilation c = compilation;
        list.Add(c.Assembly);
        list.AddRange(c.Assembly.Modules);

        return list;
    }

    #endregion

    #region "Private Helpers"

    private static void GetSourceMemberSymbols(INamespaceOrTypeSymbol symbol, List<ISymbol> list, LocalSymbolDumper localDumper)
    {
        foreach (var memberSymbol in symbol.GetMembers())
        {
            list.Add(memberSymbol);

            switch (memberSymbol.Kind)
            {
                case SymbolKind.NamedType:
                case SymbolKind.Namespace:
                    GetSourceMemberSymbols((INamespaceOrTypeSymbol)memberSymbol, list, localDumper);
                    break;
                case SymbolKind.Method:
                    var method = (IMethodSymbol)memberSymbol;
                    foreach (var parameter in method.Parameters)
                    {
                        list.Add(parameter);
                    }

                    localDumper?.GetLocalSymbols(method.GetSymbol(), list);

                    break;
                case SymbolKind.Field:
                    localDumper?.GetLocalSymbols(memberSymbol.GetSymbol<FieldSymbol>(), list);

                    break;
            }
        }
    }

    private static void GetSourceAliasSymbols(CSharpCompilation comp, List<ISymbol> list)
    {
        foreach (var tree in comp.SyntaxTrees)
        {
            var usingNodes = tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>();
            var model = comp.GetSemanticModel(tree);
            foreach (var u in usingNodes)
            {
                if (u.Alias != null)
                {
                    // var sym = model.GetSymbolInfo(u.Alias.Identifier).Symbol;
                    var sym = model.GetDeclaredSymbol(u);
                    if (sym != null && !list.Contains(sym))
                    {
                        list.Add(sym);
                    }
                }
            }
        }
    }

    #endregion

    private sealed class LocalSymbolDumper
    {
        private readonly CSharpCompilation _compilation;
        public LocalSymbolDumper(CSharpCompilation compilation)
            => _compilation = compilation;

        public void GetLocalSymbols(FieldSymbol symbol, List<ISymbol> list)
        {
            foreach (var node in symbol.DeclaringSyntaxReferences.Select(d => d.GetSyntax()))
            {
                if (node is VariableDeclaratorSyntax declarator && declarator.Initializer != null)
                {
                    var model = _compilation.GetSemanticModel(declarator.SyntaxTree);

                    // Expression
                    var df = model.AnalyzeDataFlow(declarator.Initializer.Value);
                    GetLocalAndType(df, list);

                    GetAnonymousExprSymbols(declarator.Initializer.Value, model, list);
                }
            }
        }

        public void GetLocalSymbols(MethodSymbol symbol, List<ISymbol> list)
        {
            foreach (var node in symbol.DeclaringSyntaxReferences.Select(d => d.GetSyntax()))
            {
                BlockSyntax body = null;
                if (node is BaseMethodDeclarationSyntax baseMethod)
                {
                    body = baseMethod.Body;
                }
                else if (node is AccessorDeclarationSyntax accessor)
                {
                    body = accessor.Body;
                }

                var model = _compilation.GetSemanticModel(node.SyntaxTree);

                if (body != null && body.Statements.Any())
                {
                    var df = model.AnalyzeDataFlow(body);
                    GetLocalAndType(df, list);

                    GetAnonymousTypeOrFuncSymbols(body, model, list);

                    GetLabelSymbols(body, model, list);
                }

                // C# specific (this|base access)
                if (node is ConstructorDeclarationSyntax ctor && ctor.Initializer != null)
                {
                    foreach (var a in ctor.Initializer.ArgumentList.Arguments)
                    {
                        var df = model.AnalyzeDataFlow(a.Expression);

                        // VisitLocals(arg, df);
                        list.AddRange(df.VariablesDeclared);

                        GetAnonymousExprSymbols(a.Expression, model, list);
                    }
                }
            }
        }

        private static void GetLocalAndType(DataFlowAnalysis df, List<ISymbol> list)
        {
            foreach (var v in df.VariablesDeclared)
            {
                list.Add(v);
                if (v is ILocalSymbol local && (local.Type.Kind == SymbolKind.ArrayType || local.Type.Kind == SymbolKind.PointerType))
                {
                    list.Add(local.Type);
                }
            }
        }

        private static void GetLabelSymbols(BlockSyntax body, SemanticModel model, List<ISymbol> list)
        {
            var labels = body.DescendantNodes().OfType<LabeledStatementSyntax>();
            foreach (var n in labels)
            {
                // Label: -> 'Label' is token
                var sym = model.GetDeclaredSymbol(n);
                list.Add(sym);
            }

            var swlabels = body.DescendantNodes().OfType<SwitchLabelSyntax>();
            foreach (var n in swlabels)
            {
                // label value has NO symbol, Type is expr's type
                // e.g. case "A": -> string type
                // var info1 = model.GetTypeInfo(n.Value);
                // var info2 = model.GetSymbolInfo(n.Value);
                var sym = model.GetDeclaredSymbol(n);
                list.Add(sym);
            }
        }

        private static void GetAnonymousTypeOrFuncSymbols(BlockSyntax body, SemanticModel model, List<ISymbol> list)
        {
            IEnumerable<ExpressionSyntax> exprs = body.DescendantNodes().OfType<SimpleLambdaExpressionSyntax>();
            IEnumerable<ExpressionSyntax> tmp = body.DescendantNodes().OfType<ParenthesizedLambdaExpressionSyntax>();
            exprs = exprs.Concat(tmp);
            tmp = body.DescendantNodes().OfType<AnonymousMethodExpressionSyntax>();
            exprs = exprs.Concat(tmp);

            tmp = body.DescendantNodes().OfType<AnonymousObjectCreationExpressionSyntax>();
            exprs = exprs.Concat(tmp);

            foreach (var expr in exprs)
            {
                GetAnonymousExprSymbols(expr, model, list);
            }
        }

        private static void GetAnonymousExprSymbols(ExpressionSyntax expr, SemanticModel model, List<ISymbol> list)
        {
            var kind = expr.Kind();
            if (kind is not SyntaxKind.AnonymousObjectCreationExpression and
                not SyntaxKind.AnonymousMethodExpression and
                not SyntaxKind.ParenthesizedLambdaExpression and
                not SyntaxKind.SimpleLambdaExpression)
            {
                return;
            }

            var tinfo = model.GetTypeInfo(expr);
            var conv = model.GetConversion(expr);
            if (conv.IsAnonymousFunction)
            {
                // Lambda has no Type unless in part of case expr (C# specific)
                // var f = (Func<int>)(() => { return 1; }); Type is delegate
                // method symbol
                var sinfo = model.GetSymbolInfo(expr);
                list.Add(sinfo.Symbol);
            }
            else if (tinfo.Type != null && tinfo.Type.TypeKind != TypeKind.Delegate)
            {
                // bug#12625
                // GetSymbolInfo -> .ctor (part of members)
                list.Add(tinfo.Type); // NamedType with empty name
                foreach (var m in tinfo.Type.GetMembers())
                {
                    list.Add(m);
                }
            }
        }
    }
}
