﻿// 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 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);
                    }
                }
            }
        }
    }
}
