// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.DSC;

namespace System.Management.Automation
{
    internal class CompletionContext
    {
        internal List<Ast> RelatedAsts { get; set; }

        // Only one of TokenAtCursor or TokenBeforeCursor is set
        // This is how we can tell if we're trying to complete part of something (like a member)
        // or complete an argument, where TokenBeforeCursor could be a parameter name.
        internal Token TokenAtCursor { get; set; }

        internal Token TokenBeforeCursor { get; set; }

        internal IScriptPosition CursorPosition { get; set; }

        internal PowerShellExecutionHelper Helper { get; set; }

        internal Hashtable Options { get; set; }

        internal Dictionary<string, ScriptBlock> CustomArgumentCompleters { get; set; }

        internal Dictionary<string, ScriptBlock> NativeArgumentCompleters { get; set; }

        internal string WordToComplete { get; set; }

        internal int ReplacementIndex { get; set; }

        internal int ReplacementLength { get; set; }

        internal ExecutionContext ExecutionContext { get; set; }

        internal PseudoBindingInfo PseudoBindingInfo { get; set; }

        internal TypeInferenceContext TypeInferenceContext { get; set; }

        internal bool GetOption(string option, bool @default)
        {
            if (Options == null || !Options.ContainsKey(option))
            {
                return @default;
            }

            return LanguagePrimitives.ConvertTo<bool>(Options[option]);
        }
    }

    internal class CompletionAnalysis
    {
        private readonly Ast _ast;
        private readonly Token[] _tokens;
        private readonly IScriptPosition _cursorPosition;
        private readonly Hashtable _options;

        internal CompletionAnalysis(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options)
        {
            _ast = ast;
            _tokens = tokens;
            _cursorPosition = cursorPosition;
            _options = options;
        }

        private static bool IsInterestingToken(Token token)
        {
            return token.Kind != TokenKind.NewLine && token.Kind != TokenKind.EndOfInput;
        }

        private static bool IsCursorWithinOrJustAfterExtent(IScriptPosition cursor, IScriptExtent extent)
        {
            return cursor.Offset > extent.StartOffset && cursor.Offset <= extent.EndOffset;
        }

        private static bool IsCursorRightAfterExtent(IScriptPosition cursor, IScriptExtent extent)
        {
            return cursor.Offset == extent.EndOffset;
        }

        private static bool IsCursorAfterExtentAndInTheSameLine(IScriptPosition cursor, IScriptExtent extent)
        {
            return cursor.Offset >= extent.EndOffset && extent.EndLineNumber == cursor.LineNumber;
        }

        private static bool IsCursorBeforeExtent(IScriptPosition cursor, IScriptExtent extent)
        {
            return cursor.Offset < extent.StartOffset;
        }

        private static bool IsCursorAfterExtent(IScriptPosition cursor, IScriptExtent extent)
        {
            return extent.EndOffset < cursor.Offset;
        }

        private static bool IsCursorOutsideOfExtent(IScriptPosition cursor, IScriptExtent extent)
        {
            return cursor.Offset < extent.StartOffset || cursor.Offset > extent.EndOffset;
        }

        internal readonly struct AstAnalysisContext
        {
            internal AstAnalysisContext(Token tokenAtCursor, Token tokenBeforeCursor, List<Ast> relatedAsts, int replacementIndex)
            {
                TokenAtCursor = tokenAtCursor;
                TokenBeforeCursor = tokenBeforeCursor;
                RelatedAsts = relatedAsts;
                ReplacementIndex = replacementIndex;
            }

            internal readonly Token TokenAtCursor;
            internal readonly Token TokenBeforeCursor;
            internal readonly List<Ast> RelatedAsts;
            internal readonly int ReplacementIndex;
        }

        internal static AstAnalysisContext ExtractAstContext(Ast inputAst, Token[] inputTokens, IScriptPosition cursor)
        {
            bool adjustLineAndColumn = false;
            IScriptPosition positionForAstSearch = cursor;

            Token tokenBeforeCursor = null;
            Token tokenAtCursor = InterstingTokenAtCursorOrDefault(inputTokens, cursor);
            if (tokenAtCursor == null)
            {
                tokenBeforeCursor = InterstingTokenBeforeCursorOrDefault(inputTokens, cursor);
                if (tokenBeforeCursor != null)
                {
                    positionForAstSearch = tokenBeforeCursor.Extent.EndScriptPosition;
                    adjustLineAndColumn = true;
                }
            }
            else
            {
                var stringExpandableToken = tokenAtCursor as StringExpandableToken;
                if (stringExpandableToken?.NestedTokens != null)
                {
                    tokenAtCursor = InterstingTokenAtCursorOrDefault(stringExpandableToken.NestedTokens, cursor) ?? stringExpandableToken;
                }
            }

            int replacementIndex = adjustLineAndColumn ? cursor.Offset : 0;
            List<Ast> relatedAsts = AstSearcher.FindAll(
                inputAst,
                ast => IsCursorWithinOrJustAfterExtent(positionForAstSearch, ast.Extent),
                searchNestedScriptBlocks: true).ToList();

            if (relatedAsts.Count == 0)
            {
                relatedAsts.Add(inputAst);
            }

            // If the last ast is an unnamed block that starts with "param" the cursor is inside a param block.
            // To avoid adding special handling to all the completers that look at the last ast, we remove it here because it's not useful for completion.
            if (relatedAsts[^1].Extent.Text.StartsWith("param", StringComparison.OrdinalIgnoreCase)
                && relatedAsts[^1] is NamedBlockAst namedBlock && namedBlock.Unnamed)
            {
                relatedAsts.RemoveAt(relatedAsts.Count - 1);
            }

            Diagnostics.Assert(tokenAtCursor == null || tokenBeforeCursor == null, "Only one of these tokens can be non-null");

            return new AstAnalysisContext(tokenAtCursor, tokenBeforeCursor, relatedAsts, replacementIndex);
        }

        internal CompletionContext CreateCompletionContext(PowerShell powerShell)
        {
            var typeInferenceContext = new TypeInferenceContext(powerShell);
            return InitializeCompletionContext(typeInferenceContext);
        }

        internal CompletionContext CreateCompletionContext(TypeInferenceContext typeInferenceContext)
        {
            return InitializeCompletionContext(typeInferenceContext);
        }

        private CompletionContext InitializeCompletionContext(TypeInferenceContext typeInferenceContext)
        {
            var astContext = ExtractAstContext(_ast, _tokens, _cursorPosition);

            typeInferenceContext.CurrentTypeDefinitionAst ??= Ast.GetAncestorTypeDefinitionAst(astContext.RelatedAsts.Last());

            ExecutionContext executionContext = typeInferenceContext.ExecutionContext;

            return new CompletionContext
            {
                Options = _options,
                CursorPosition = _cursorPosition,
                TokenAtCursor = astContext.TokenAtCursor,
                TokenBeforeCursor = astContext.TokenBeforeCursor,
                RelatedAsts = astContext.RelatedAsts,
                ReplacementIndex = astContext.ReplacementIndex,
                ExecutionContext = executionContext,
                TypeInferenceContext = typeInferenceContext,
                Helper = typeInferenceContext.Helper,
                CustomArgumentCompleters = executionContext.CustomArgumentCompleters,
                NativeArgumentCompleters = executionContext.NativeArgumentCompleters,
            };
        }

        private static Token InterstingTokenAtCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
        {
            for (int i = tokens.Count - 1; i >= 0; --i)
            {
                Token token = tokens[i];
                if (IsCursorWithinOrJustAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
                {
                    return token;
                }
            }

            return null;
        }

        private static Token InterstingTokenBeforeCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
        {
            for (int i = tokens.Count - 1; i >= 0; --i)
            {
                Token token = tokens[i];
                if (IsCursorAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
                {
                    return token;
                }
            }

            return null;
        }

        private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosition cursorPosition)
        {
            var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorRightAfterExtent(cursorPosition, ast.Extent), searchNestedScriptBlocks: true);
            return asts.LastOrDefault();
        }

        #region Special Cases

        /// <summary>
        /// Check if we should complete file names for "switch -file"
        /// </summary>
        private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCursor)
        {
            Tuple<Token, Ast> fileConditionTuple;

            var errorStatement = lastAst as ErrorStatementAst;
            if (errorStatement != null && errorStatement.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null &&
                errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple))
            {
                // Handle "switch -file <tab>"
                return fileConditionTuple.Item1.Extent.EndOffset == tokenBeforeCursor.Extent.EndOffset;
            }

            if (lastAst.Parent is CommandExpressionAst)
            {
                // Handle "switch -file m<tab>" or "switch -file *.ps1<tab>"
                if (!(lastAst.Parent.Parent is PipelineAst pipeline))
                {
                    return false;
                }

                errorStatement = pipeline.Parent as ErrorStatementAst;
                if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null)
                {
                    return false;
                }

                return (errorStatement.Kind.Kind.Equals(TokenKind.Switch) &&
                        errorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline);
            }

            return false;
        }

        private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst)
        {
            if (tokenAtCursor.Kind == TokenKind.Minus)
            {
                return lastAst is BinaryExpressionAst;
            }
            else if (tokenAtCursor.Kind == TokenKind.Parameter)
            {
                if (lastAst is CommandParameterAst)
                    return lastAst.Parent is ExpressionAst;
            }

            return false;
        }

        private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, Token token, out TokenKind kind)
        {
            kind = TokenKind.Unknown;

            // Handle "switch -f<tab>"
            var errorStatement = lastAst as ErrorStatementAst;
            if (errorStatement != null && errorStatement.Kind != null)
            {
                switch (errorStatement.Kind.Kind)
                {
                    case TokenKind.Switch:
                        kind = TokenKind.Switch;
                        return true;
                    default:
                        break;
                }
            }

            // Handle "switch -<tab>". Skip cases like "switch ($a) {} -<tab> "
            var scriptBlockAst = scriptAst as ScriptBlockAst;
            if (token != null && token.Kind == TokenKind.Minus && scriptBlockAst != null)
            {
                var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorAfterExtent(token.Extent.StartScriptPosition, ast.Extent), searchNestedScriptBlocks: true);

                Ast last = asts.LastOrDefault();
                errorStatement = null;

                while (last != null)
                {
                    errorStatement = last as ErrorStatementAst;
                    if (errorStatement != null) { break; }

                    last = last.Parent;
                }

                if (errorStatement != null && errorStatement.Kind != null)
                {
                    switch (errorStatement.Kind.Kind)
                    {
                        case TokenKind.Switch:

                            Tuple<Token, Ast> value;
                            if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value))
                            {
                                if (IsTokenTheSame(value.Item1, token))
                                {
                                    kind = TokenKind.Switch;
                                    return true;
                                }
                            }

                            break;

                        default:
                            break;
                    }
                }
            }

            return false;
        }

        private static bool IsTokenTheSame(Token x, Token y)
        {
            if (x.Kind == y.Kind && x.TokenFlags == y.TokenFlags &&
                x.Extent.StartLineNumber == y.Extent.StartLineNumber &&
                x.Extent.StartColumnNumber == y.Extent.StartColumnNumber &&
                x.Extent.EndLineNumber == y.Extent.EndLineNumber &&
                x.Extent.EndColumnNumber == y.Extent.EndColumnNumber)
            {
                return true;
            }

            return false;
        }

        #endregion Special Cases

        internal List<CompletionResult> GetResults(PowerShell powerShell, out int replacementIndex, out int replacementLength)
        {
            var completionContext = CreateCompletionContext(powerShell);

            PSLanguageMode? previousLanguageMode = null;
            try
            {
                // Tab expansion is called from a trusted function - we should apply ConstrainedLanguage if necessary.
                if (completionContext.ExecutionContext.HasRunspaceEverUsedConstrainedLanguageMode)
                {
                    previousLanguageMode = completionContext.ExecutionContext.LanguageMode;
                    completionContext.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage;
                }

                return GetResultHelper(completionContext, out replacementIndex, out replacementLength);
            }
            finally
            {
                if (previousLanguageMode.HasValue)
                {
                    completionContext.ExecutionContext.LanguageMode = previousLanguageMode.Value;
                }
            }
        }

        internal List<CompletionResult> GetResultHelper(CompletionContext completionContext, out int replacementIndex, out int replacementLength)
        {
            replacementIndex = -1;
            replacementLength = -1;

            var tokenAtCursor = completionContext.TokenAtCursor;
            var lastAst = completionContext.RelatedAsts.Last();
            List<CompletionResult> result = null;
            if (tokenAtCursor != null)
            {
                replacementIndex = tokenAtCursor.Extent.StartScriptPosition.Offset;
                replacementLength = tokenAtCursor.Extent.EndScriptPosition.Offset - replacementIndex;

                completionContext.ReplacementIndex = replacementIndex;
                completionContext.ReplacementLength = replacementLength;

                switch (tokenAtCursor.Kind)
                {
                    case TokenKind.Variable:
                    case TokenKind.SplattedVariable:
                        completionContext.WordToComplete = ((VariableToken)tokenAtCursor).VariablePath.UserPath;
                        result = CompletionCompleters.CompleteVariable(completionContext);
                        break;

                    case TokenKind.Multiply:
                    case TokenKind.Generic:
                    case TokenKind.MinusMinus: // for native commands '--'
                    case TokenKind.Identifier:
                        if (!tokenAtCursor.TokenFlags.HasFlag(TokenFlags.TypeName))
                        {
                            result = CompleteUsingKeywords(completionContext.CursorPosition.Offset, _tokens, ref replacementIndex, ref replacementLength);
                            if (result is not null)
                            {
                                return result;
                            }

                            result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength);
                        }

                        break;

                    case TokenKind.Parameter:
                        completionContext.WordToComplete = tokenAtCursor.Text;
                        var cmdAst = lastAst.Parent as CommandAst;
                        if (lastAst is StringConstantExpressionAst && cmdAst != null && cmdAst.CommandElements.Count == 1)
                        {
                            result = CompleteFileNameAsCommand(completionContext);
                            break;
                        }

                        TokenKind statementKind;
                        if (CompleteAgainstStatementFlags(null, lastAst, null, out statementKind))
                        {
                            result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
                            break;
                        }

                        if (CompleteOperator(tokenAtCursor, lastAst))
                        {
                            result = CompletionCompleters.CompleteOperator(completionContext.WordToComplete);
                            break;
                        }

                        // Handle scenarios like this: dir -path:<tab>
                        if (completionContext.WordToComplete.EndsWith(':'))
                        {
                            replacementIndex = tokenAtCursor.Extent.EndScriptPosition.Offset;
                            replacementLength = 0;

                            completionContext.WordToComplete = string.Empty;
                            result = CompletionCompleters.CompleteCommandArgument(completionContext);
                        }
                        else
                        {
                            result = CompletionCompleters.CompleteCommandParameter(completionContext);
                        }

                        break;

                    case TokenKind.Dot:
                    case TokenKind.ColonColon:
                    case TokenKind.QuestionDot:
                        replacementIndex += tokenAtCursor.Text.Length;
                        replacementLength = 0;
                        result = CompletionCompleters.CompleteMember(completionContext, @static: tokenAtCursor.Kind == TokenKind.ColonColon, ref replacementLength);

                        break;

                    case TokenKind.Comment:
                        completionContext.WordToComplete = tokenAtCursor.Text;
                        result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength);
                        break;

                    case TokenKind.StringExpandable:
                    case TokenKind.StringLiteral:
                        // Search to see if we're looking at an assignment
                        if (lastAst.Parent is CommandExpressionAst
                            && lastAst.Parent.Parent is AssignmentStatementAst assignmentAst)
                        {
                            // Handle scenarios like `$ErrorActionPreference = '<tab>`
                            if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst, out List<CompletionResult> completions))
                            {
                                return completions;
                            }
                        }
                        else if (lastAst.Parent is BinaryExpressionAst binaryExpression)
                        {
                            completionContext.WordToComplete = (tokenAtCursor as StringToken).Value;
                            result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left);
                            if (result.Count > 0)
                            {
                                return result;
                            }
                        }
                        else if (lastAst.Parent is IndexExpressionAst indexExpressionAst)
                        {
                            // Handles quoted string inside index expression like: $PSVersionTable["<Tab>"]
                            completionContext.WordToComplete = (tokenAtCursor as StringToken).Value;
                            return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpressionAst.Target);
                        }

                        result = GetResultForString(completionContext, ref replacementIndex, ref replacementLength);
                        break;

                    case TokenKind.RBracket:
                        if (lastAst is TypeExpressionAst)
                        {
                            var targetExpr = (TypeExpressionAst)lastAst;
                            var memberResult = new List<CompletionResult>();

                            CompletionCompleters.CompleteMemberHelper(
                                true,
                                "*",
                                targetExpr,
                                completionContext, memberResult);

                            if (memberResult.Count > 0)
                            {
                                replacementIndex++;
                                replacementLength = 0;
                                result = (from entry in memberResult
                                          let completionText = TokenKind.ColonColon.Text() + entry.CompletionText
                                          select new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip)).ToList();
                            }
                        }

                        break;

                    case TokenKind.Comma:
                        // Handle array elements such as the followings:
                        //  - `dir .\cd,<tab>`
                        //  - `dir -Path: .\cd,<tab>`
                        //  - `dir .\abc.txt,<tab> -File`
                        //  - `dir -Path .\abc.txt,<tab> -File`
                        //  - `dir -Path: .\abc.txt,<tab> -File`
                        if (lastAst is ErrorExpressionAst or ArrayLiteralAst &&
                            lastAst.Parent is CommandAst or CommandParameterAst)
                        {
                            replacementIndex += replacementLength;
                            replacementLength = 0;

                            result = CompletionCompleters.CompleteCommandArgument(completionContext);
                        }
                        else if (lastAst is AttributeAst)
                        {
                            completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
                            completionContext.ReplacementLength = replacementLength = 0;
                            result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
                        }
                        else
                        {
                            //
                            // Handle auto completion for enum/dependson property of DSC resource,
                            // cursor is right after ','
                            //
                            // Configuration config
                            // {
                            //     User test
                            //     {
                            //         DependsOn=@('[user]x',|)
                            //
                            result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out _);
                        }

                        break;
                    case TokenKind.AtCurly:
                        // Handle scenarios such as 'Sort-Object @{<tab>' and  'gci | Format-Table @{'
                        result = GetResultForHashtable(completionContext);
                        replacementIndex += 2;
                        replacementLength = 0;
                        break;

                    case TokenKind.Semi:
                        // Handle scenarios such as 'gci | Format-Table @{Label=...;<tab>'
                        if (lastAst is HashtableAst)
                        {
                            result = GetResultForHashtable(completionContext);
                            replacementIndex += 1;
                            replacementLength = 0;
                        }

                        break;

                    case TokenKind.Number:
                        // Handle scenarios such as Get-Process -Id 5<tab> || Get-Process -Id 5210, 3<tab> || Get-Process -Id: 5210, 3<tab>
                        if (lastAst is ConstantExpressionAst &&
                            (lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst ||
                            (lastAst.Parent is ArrayLiteralAst &&
                             (lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))
                        {
                            completionContext.WordToComplete = tokenAtCursor.Text;
                            result = CompletionCompleters.CompleteCommandArgument(completionContext);

                            replacementIndex = completionContext.ReplacementIndex;
                            replacementLength = completionContext.ReplacementLength;
                        }
                        else if (lastAst.Parent is CommandExpressionAst
                            && lastAst.Parent.Parent is AssignmentStatementAst assignmentAst2)
                        {
                            // Handle scenarios like '[ValidateSet(11,22)][int]$i = 11; $i = 2<tab>'
                            if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))
                            {
                                result = completions;
                            }
                        }

                        break;

                    case TokenKind.Redirection:
                        // Handle file name completion after the redirection operator: gps ><tab> || gps >><tab> || dir con 2><tab> || dir con 2>><tab>
                        if (lastAst is ErrorExpressionAst && lastAst.Parent is FileRedirectionAst)
                        {
                            completionContext.WordToComplete = string.Empty;
                            completionContext.ReplacementIndex = (replacementIndex += tokenAtCursor.Text.Length);
                            completionContext.ReplacementLength = replacementLength = 0;
                            result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
                        }

                        break;

                    case TokenKind.Minus:
                        // Handle operator completion: 55 -<tab> || "string" -<tab> || (Get-Something) -<tab>
                        if (CompleteOperator(tokenAtCursor, lastAst))
                        {
                            result = CompletionCompleters.CompleteOperator(string.Empty);
                            break;
                        }

                        // Handle the flag completion for statements, such as the switch statement
                        if (CompleteAgainstStatementFlags(completionContext.RelatedAsts[0], null, tokenAtCursor, out statementKind))
                        {
                            completionContext.WordToComplete = tokenAtCursor.Text;
                            result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
                            break;
                        }

                        break;

                    case TokenKind.DynamicKeyword:
                        {
                            DynamicKeywordStatementAst keywordAst;
                            ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst(
                                completionContext.CursorPosition, lastAst, out keywordAst);
                            Diagnostics.Assert(configureAst != null, "ConfigurationDefinitionAst should never be null");
                            bool matched = false;
                            completionContext.WordToComplete = tokenAtCursor.Text.Trim();
                            // Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst
                            return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out matched);
                        }
                    case TokenKind.Equals:
                    case TokenKind.AtParen:
                    case TokenKind.LParen:
                        {
                            if (lastAst is AttributeAst)
                            {
                                completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
                                completionContext.ReplacementLength = replacementLength = 0;
                                result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
                            }
                            else if (lastAst is HashtableAst hashTableAst && lastAst.Parent is not DynamicKeywordStatementAst && CheckForPendingAssignment(hashTableAst))
                            {
                                // Handle scenarios such as 'gci | Format-Table @{Label=<tab>' if incomplete parsing of the assignment.
                                return null;
                            }
                            else if (lastAst is AssignmentStatementAst assignmentAst2)
                            {
                                completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
                                completionContext.ReplacementLength = replacementLength = 0;

                                // Handle scenarios like '$ErrorActionPreference =<tab>'
                                if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))
                                {
                                    return completions;
                                }
                            }
                            else
                            {
                                // Handle scenarios such as 'configuration foo { File ab { Attributes ='
                                // (auto completion for enum/dependson property of DSC resource),
                                // cursor is right after '=', '(' or '@('
                                //
                                // Configuration config
                                // {
                                //     User test
                                //     {
                                //         DependsOn=|
                                //         DependsOn=@(|)
                                //         DependsOn=(|
                                //
                                result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out _);
                            }

                            break;
                        }

                    case TokenKind.Format:
                    case TokenKind.Not:
                    case TokenKind.Bnot:
                    case TokenKind.And:
                    case TokenKind.Or:
                    case TokenKind.Xor:
                    case TokenKind.Band:
                    case TokenKind.Bor:
                    case TokenKind.Bxor:
                    case TokenKind.Join:
                    case TokenKind.Ieq:
                    case TokenKind.Ine:
                    case TokenKind.Ige:
                    case TokenKind.Igt:
                    case TokenKind.Ilt:
                    case TokenKind.Ile:
                    case TokenKind.Ilike:
                    case TokenKind.Inotlike:
                    case TokenKind.Imatch:
                    case TokenKind.Inotmatch:
                    case TokenKind.Ireplace:
                    case TokenKind.Icontains:
                    case TokenKind.Inotcontains:
                    case TokenKind.Iin:
                    case TokenKind.Inotin:
                    case TokenKind.Isplit:
                    case TokenKind.Ceq:
                    case TokenKind.Cne:
                    case TokenKind.Cge:
                    case TokenKind.Cgt:
                    case TokenKind.Clt:
                    case TokenKind.Cle:
                    case TokenKind.Clike:
                    case TokenKind.Cnotlike:
                    case TokenKind.Cmatch:
                    case TokenKind.Cnotmatch:
                    case TokenKind.Creplace:
                    case TokenKind.Ccontains:
                    case TokenKind.Cnotcontains:
                    case TokenKind.Cin:
                    case TokenKind.Cnotin:
                    case TokenKind.Csplit:
                    case TokenKind.Is:
                    case TokenKind.IsNot:
                    case TokenKind.As:
                    case TokenKind.Shl:
                    case TokenKind.Shr:
                        result = CompletionCompleters.CompleteOperator(tokenAtCursor.Text);
                        break;

                    case TokenKind.LBracket:
                        if (lastAst.Parent is IndexExpressionAst indexExpression)
                        {
                            // Handles index expression with cursor right after lbracket like: $PSVersionTable[<Tab>]
                            completionContext.WordToComplete = string.Empty;
                            result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
                            if (result.Count > 0)
                            {
                                replacementIndex++;
                                replacementLength--;
                            }
                        }
                        break;
                    default:
                        result = CompleteUsingKeywords(completionContext.CursorPosition.Offset, _tokens, ref replacementIndex, ref replacementLength);
                        if (result is not null)
                        {
                            return result;
                        }

                        if ((tokenAtCursor.TokenFlags & TokenFlags.Keyword) != 0)
                        {
                            completionContext.WordToComplete = tokenAtCursor.Text;

                            // Handle the file name completion
                            result = CompleteFileNameAsCommand(completionContext);

                            // Handle the command name completion
                            var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);
                            if (commandNameResult != null && commandNameResult.Count > 0)
                            {
                                result.AddRange(commandNameResult);
                            }
                        }
                        else
                        {
                            replacementIndex = -1;
                            replacementLength = -1;
                        }

                        break;
                }
            }
            else
            {
                IScriptPosition cursor = completionContext.CursorPosition;
                bool isCursorLineEmpty = string.IsNullOrWhiteSpace(cursor.Line);
                var tokenBeforeCursor = completionContext.TokenBeforeCursor;
                bool isLineContinuationBeforeCursor = false;
                if (tokenBeforeCursor != null)
                {
                    //
                    // Handle following scenario, cursor is in next line and after a command call,
                    // we need to skip the command call autocompletion if there is no backtick character
                    // in the end of the previous line, since backtick means command call continues to the next line
                    //
                    // Configuration config
                    // {
                    //     User test
                    //     {
                    //         DependsOn=zzz
                    //         |
                    //
                    isLineContinuationBeforeCursor = completionContext.TokenBeforeCursor.Kind == TokenKind.LineContinuation;
                }

                bool skipAutoCompleteForCommandCall = isCursorLineEmpty && !isLineContinuationBeforeCursor;
                bool lastAstIsExpressionAst = lastAst is ExpressionAst;
                if (!skipAutoCompleteForCommandCall &&
                    (lastAst is CommandParameterAst || lastAst is CommandAst ||
                    (lastAstIsExpressionAst && lastAst.Parent is CommandAst) ||
                    (lastAstIsExpressionAst && lastAst.Parent is CommandParameterAst) ||
                    (lastAstIsExpressionAst && lastAst.Parent is ArrayLiteralAst &&
                    (lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))
                {
                    completionContext.WordToComplete = string.Empty;

                    var hashTableAst = lastAst as HashtableAst;

                    // Do not do any tab completion if we have a hash table
                    // and an assignment is pending.  For cases like:
                    //   new-object System.Drawing.Point -prop @{ X=  -> Tab should not complete
                    // Note: This check works when all statements preceding the last are complete,
                    //       but if a preceding statement is incomplete this test fails because
                    //       the Ast mixes the statements due to incomplete parsing.
                    //   e.g.,
                    //   new-object System.Drawing.Point -prop @{ X = 100; Y =      <- Incomplete line
                    //   new-object new-object System.Drawing.Point -prop @{ X =    <- Tab will yield hash properties.
                    if (hashTableAst != null &&
                        CheckForPendingAssignment(hashTableAst))
                    {
                        return result;
                    }

                    if (hashTableAst != null)
                    {
                        completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset;
                        completionContext.ReplacementLength = replacementLength = 0;
                        result = CompletionCompleters.CompleteHashtableKey(completionContext, hashTableAst);
                    }
                    else
                    {
                        result = CompletionCompleters.CompleteCommandArgument(completionContext);
                        replacementIndex = completionContext.ReplacementIndex;
                        replacementLength = completionContext.ReplacementLength;
                    }
                }
                else
                {
                    //
                    // Handle completion of empty line within configuration statement
                    // Ignore the auto completion if there is a backtick character in previous line
                    //
                    bool cursorAtLineContinuation;
                    if ((tokenAtCursor != null && tokenAtCursor.Kind == TokenKind.LineContinuation) ||
                        (tokenBeforeCursor != null && tokenBeforeCursor.Kind == TokenKind.LineContinuation))
                        cursorAtLineContinuation = true;
                    else
                        cursorAtLineContinuation = false;
                    if (isCursorLineEmpty && !cursorAtLineContinuation)
                    {
                        //
                        // Handle following scenario, both Configuration and DSC resource 'User' are not complete
                        // Check Hashtable first, and then fallback to configuration
                        //
                        // Configuration config
                        // {
                        //     User test
                        //     {
                        //         DependsOn=''
                        //         |
                        result = GetResultForHashtable(completionContext);
                        if (result == null || result.Count == 0)
                        {
                            DynamicKeywordStatementAst keywordAst;
                            ConfigurationDefinitionAst configAst = GetAncestorConfigurationAstAndKeywordAst(cursor, lastAst, out keywordAst);
                            if (configAst != null)
                            {
                                bool matched;
                                result = GetResultForIdentifierInConfiguration(completionContext, configAst, keywordAst, out matched);
                            }
                        }

                        // Handles following scenario where user is tab completing a member on an empty line:
                        // "Hello".
                        // <Tab>
                        if ((result is null || result.Count == 0) && tokenBeforeCursor is not null)
                        {
                            switch (completionContext.TokenBeforeCursor.Kind)
                            {

                                case TokenKind.Dot:
                                case TokenKind.ColonColon:
                                case TokenKind.QuestionDot:
                                    replacementIndex = cursor.Offset;
                                    replacementLength = 0;
                                    result = CompletionCompleters.CompleteMember(completionContext, @static: completionContext.TokenBeforeCursor.Kind == TokenKind.ColonColon, ref replacementLength);
                                    break;

                                case TokenKind.LParen:
                                case TokenKind.Comma:
                                    if (lastAst is AttributeAst)
                                    {
                                        result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
                                    }
                                    break;

                                case TokenKind.Ieq:
                                case TokenKind.Ceq:
                                case TokenKind.Ine:
                                case TokenKind.Cne:
                                case TokenKind.Ilike:
                                case TokenKind.Clike:
                                case TokenKind.Inotlike:
                                case TokenKind.Cnotlike:
                                case TokenKind.Imatch:
                                case TokenKind.Cmatch:
                                case TokenKind.Inotmatch:
                                case TokenKind.Cnotmatch:
                                    if (lastAst is BinaryExpressionAst binaryExpression)
                                    {
                                        completionContext.WordToComplete = string.Empty;
                                        result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left);
                                    }
                                    break;

                                case TokenKind.LBracket:
                                    if (lastAst.Parent is IndexExpressionAst indexExpression)
                                    {
                                        // Handles index expression where cursor is on a new line after the lbracket like: $PSVersionTable[\n<Tab>]
                                        completionContext.WordToComplete = string.Empty;
                                        result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
                                    }
                                    break;

                                default:
                                    break;
                            }
                        }
                    }
                    else if (completionContext.TokenAtCursor == null)
                    {
                        if (tokenBeforeCursor != null)
                        {
                            //
                            // Handle auto completion for enum/dependson property of DSC resource,
                            // cursor is after '=', ',', '(', or '@('
                            //
                            // Configuration config
                            // {
                            //     User test
                            //     {
                            //         DependsOn= |
                            //         DependsOn=@('[user]x', |)
                            //         DependsOn=@( |)
                            //         DependsOn=(|
                            //
                            switch (tokenBeforeCursor.Kind)
                            {
                                case TokenKind.Equals:
                                case TokenKind.Comma:
                                case TokenKind.AtParen:
                                case TokenKind.LParen:
                                    {
                                        if (lastAst is AssignmentStatementAst assignmentAst)
                                        {
                                            // Handle scenarios like '$ErrorActionPreference = <tab>'
                                            if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst, out result))
                                            {
                                                break;
                                            }
                                        }

                                        if (lastAst is AttributeAst)
                                        {
                                            completionContext.ReplacementLength = replacementLength = 0;
                                            result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
                                            break;
                                        }

                                        result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out _);
                                        break;
                                    }

                                case TokenKind.Break:
                                case TokenKind.Continue:
                                    {
                                        if ((lastAst is BreakStatementAst breakStatement && breakStatement.Label is null)
                                            || (lastAst is ContinueStatementAst continueStatement && continueStatement.Label is null))
                                        {
                                            result = CompleteLoopLabel(completionContext);
                                        }
                                        break;
                                    }

                                case TokenKind.Using:
                                    return CompleteUsingKeywords(completionContext.CursorPosition.Offset, _tokens, ref replacementIndex, ref replacementLength);

                                case TokenKind.Dot:
                                case TokenKind.ColonColon:
                                case TokenKind.QuestionDot:
                                    // Handles following scenario with whitespace after member access token: "Hello". <Tab>
                                    replacementIndex = cursor.Offset;
                                    replacementLength = 0;
                                    result = CompletionCompleters.CompleteMember(completionContext, @static: tokenBeforeCursor.Kind == TokenKind.ColonColon, ref replacementLength);
                                    if (result is not null && result.Count > 0)
                                    {
                                        return result;
                                    }
                                    break;

                                case TokenKind.Ieq:
                                case TokenKind.Ceq:
                                case TokenKind.Ine:
                                case TokenKind.Cne:
                                case TokenKind.Ilike:
                                case TokenKind.Clike:
                                case TokenKind.Inotlike:
                                case TokenKind.Cnotlike:
                                case TokenKind.Imatch:
                                case TokenKind.Cmatch:
                                case TokenKind.Inotmatch:
                                case TokenKind.Cnotmatch:
                                    if (lastAst is BinaryExpressionAst binaryExpression)
                                    {
                                        completionContext.WordToComplete = string.Empty;
                                        result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left);
                                    }
                                    break;

                                case TokenKind.LBracket:
                                    if (lastAst.Parent is IndexExpressionAst indexExpression)
                                    {
                                        // Handles index expression with whitespace between lbracket and cursor like: $PSVersionTable[ <Tab>]
                                        completionContext.WordToComplete = string.Empty;
                                        result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
                                    }
                                    break;

                                default:
                                    break;
                            }
                        }
                    }

                    if (result != null && result.Count > 0)
                    {
                        completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset;
                        completionContext.ReplacementLength = replacementLength = 0;
                    }
                    else
                    {
                        bool needFileCompletion = false;
                        if (lastAst is ErrorExpressionAst && lastAst.Parent is FileRedirectionAst)
                        {
                            // Handle file name completion after redirection operator: gps > <tab>
                            needFileCompletion = true;
                        }
                        else if (lastAst is ErrorStatementAst && CompleteAgainstSwitchFile(lastAst, completionContext.TokenBeforeCursor))
                        {
                            // Handle file name completion after "switch -file": switch -file <tab>
                            needFileCompletion = true;
                        }

                        if (needFileCompletion)
                        {
                            completionContext.WordToComplete = string.Empty;
                            result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));

                            replacementIndex = completionContext.ReplacementIndex;
                            replacementLength = completionContext.ReplacementLength;
                        }
                    }
                }
            }

            if (result == null || result.Count == 0)
            {
                var typeAst = completionContext.RelatedAsts.OfType<TypeExpressionAst>().FirstOrDefault();
                TypeName typeNameToComplete = null;
                if (typeAst != null)
                {
                    typeNameToComplete = FindTypeNameToComplete(typeAst.TypeName, _cursorPosition);
                }
                else
                {
                    var typeConstraintAst = completionContext.RelatedAsts.OfType<TypeConstraintAst>().FirstOrDefault();
                    if (typeConstraintAst != null)
                    {
                        typeNameToComplete = FindTypeNameToComplete(typeConstraintAst.TypeName, _cursorPosition);
                    }
                }

                if (typeNameToComplete is null && tokenAtCursor?.TokenFlags.HasFlag(TokenFlags.TypeName) == true)
                {
                    typeNameToComplete = new TypeName(tokenAtCursor.Extent, tokenAtCursor.Text);
                }

                if (typeNameToComplete != null)
                {
                    // See if the typename to complete really is within the typename, and if so, which one, in the case of generics.

                    replacementIndex = typeNameToComplete.Extent.StartOffset;
                    replacementLength = typeNameToComplete.Extent.EndOffset - replacementIndex;
                    completionContext.WordToComplete = typeNameToComplete.FullName;
                    result = CompletionCompleters.CompleteType(completionContext);
                }
            }

            if (result == null || result.Count == 0)
            {
                result = GetResultForHashtable(completionContext);
                // Handles the following scenario: [ipaddress]@{Address=""; <Tab> }
                if (result?.Count > 0)
                {
                    replacementIndex = completionContext.CursorPosition.Offset;
                    replacementLength = 0;
                }
            }

            if (result == null || result.Count == 0)
            {
                // Handle special file completion scenarios: .\+file.txt -> +<tab>
                string input = completionContext.RelatedAsts[0].Extent.Text;
                if (Regex.IsMatch(input, @"^[\S]+$") && completionContext.RelatedAsts.Count > 0 && completionContext.RelatedAsts[0] is ScriptBlockAst)
                {
                    replacementIndex = completionContext.RelatedAsts[0].Extent.StartScriptPosition.Offset;
                    replacementLength = completionContext.RelatedAsts[0].Extent.EndScriptPosition.Offset - replacementIndex;

                    completionContext.WordToComplete = input;
                    result = CompleteFileNameAsCommand(completionContext);
                }
            }

            return result;
        }

        // Helper method to auto complete hashtable key
        private static List<CompletionResult> GetResultForHashtable(CompletionContext completionContext)
        {
            Ast lastRelatedAst = null;
            var cursorPosition = completionContext.CursorPosition;

            // Enumeration is used over the LastAst pattern because empty lines following a key-value pair will set LastAst to the value.
            // Example:
            // @{
            //     Key1="Value1"
            //     <Tab>
            // }
            // In this case the last 3 Asts will be StringConstantExpression, CommandExpression, and Pipeline instead of the expected Hashtable
            for (int i = completionContext.RelatedAsts.Count - 1; i >= 0; i--)
            {
                Ast ast = completionContext.RelatedAsts[i];
                if (cursorPosition.Offset >= ast.Extent.StartOffset && cursorPosition.Offset <= ast.Extent.EndOffset)
                {
                    lastRelatedAst = ast;
                    break;
                }
            }

            if (lastRelatedAst is HashtableAst hashtableAst)
            {
                // Cursor is just after the hashtable: @{}<Tab>
                if (completionContext.TokenAtCursor is not null && completionContext.TokenAtCursor.Kind == TokenKind.RCurly)
                {
                    return null;
                }

                bool cursorIsWithinOrOnSameLineAsKeypair = false;
                foreach (var pair in hashtableAst.KeyValuePairs)
                {
                    if (cursorPosition.Offset >= pair.Item1.Extent.StartOffset
                        && (cursorPosition.Offset <= pair.Item2.Extent.EndOffset || cursorPosition.LineNumber == pair.Item2.Extent.EndLineNumber))
                    {
                        cursorIsWithinOrOnSameLineAsKeypair = true;
                        break;
                    }
                }

                if (cursorIsWithinOrOnSameLineAsKeypair)
                {
                    var tokenBeforeOrAtCursor = completionContext.TokenBeforeCursor ?? completionContext.TokenAtCursor;
                    if (tokenBeforeOrAtCursor.Kind != TokenKind.Semi)
                    {
                        return null;
                    }
                }
                completionContext.ReplacementIndex = completionContext.CursorPosition.Offset;
                completionContext.ReplacementLength = 0;
                return CompletionCompleters.CompleteHashtableKey(completionContext, hashtableAst);
            }

            return null;
        }

        // Helper method to look for an incomplete assignment pair in hash table.
        private static bool CheckForPendingAssignment(HashtableAst hashTableAst)
        {
            foreach (var keyValue in hashTableAst.KeyValuePairs)
            {
                if (keyValue.Item2 is ErrorStatementAst)
                {
                    // This indicates the assignment has not completed.
                    return true;
                }
            }

            return false;
        }

        internal static TypeName FindTypeNameToComplete(ITypeName type, IScriptPosition cursor)
        {
            var typeName = type as TypeName;
            if (typeName != null)
            {
                // If the cursor is at the start offset, it's not really inside, so return null.
                // If the cursor is at the end offset, it's not really inside, but it's just before the cursor,
                // we don want to complete it.
                return (cursor.Offset > type.Extent.StartOffset && cursor.Offset <= type.Extent.EndOffset)
                           ? typeName
                           : null;
            }

            var genericTypeName = type as GenericTypeName;
            if (genericTypeName != null)
            {
                typeName = FindTypeNameToComplete(genericTypeName.TypeName, cursor);
                if (typeName != null)
                    return typeName;
                foreach (var t in genericTypeName.GenericArguments)
                {
                    typeName = FindTypeNameToComplete(t, cursor);
                    if (typeName != null)
                        return typeName;
                }

                return null;
            }

            var arrayTypeName = type as ArrayTypeName;
            if (arrayTypeName != null)
            {
                return FindTypeNameToComplete(arrayTypeName.ElementType, cursor) ?? null;
            }

            return null;
        }

        private static string GetFirstLineSubString(string stringToComplete, out bool hasNewLine)
        {
            hasNewLine = false;
            if (!string.IsNullOrEmpty(stringToComplete))
            {
                var index = stringToComplete.AsSpan().IndexOfAny('\r', '\n');
                if (index >= 0)
                {
                    stringToComplete = stringToComplete.Substring(0, index);
                    hasNewLine = true;
                }
            }

            return stringToComplete;
        }

        private static Tuple<ExpressionAst, StatementAst> GetHashEntryContainsCursor(
            IScriptPosition cursor,
            HashtableAst hashTableAst,
            bool isCursorInString)
        {
            Tuple<ExpressionAst, StatementAst> keyValuePairWithCursor = null;
            foreach (var kvp in hashTableAst.KeyValuePairs)
            {
                if (IsCursorWithinOrJustAfterExtent(cursor, kvp.Item2.Extent))
                {
                    keyValuePairWithCursor = kvp;
                    break;
                }

                if (!isCursorInString)
                {
                    //
                    // Handle following case, cursor is after '=' but before next key value pair,
                    // next key value pair will be treated as kvp.Item2 of 'Ensure' key
                    //
                    //    configuration foo
                    //    {
                    //        File foo
                    //        {
                    //            DestinationPath = "\foo.txt"
                    //            Ensure = |
                    //            DependsOn =@("[User]x")
                    //        }
                    //    }
                    //
                    if (kvp.Item2.Extent.StartLineNumber > kvp.Item1.Extent.EndLineNumber &&
                        IsCursorAfterExtentAndInTheSameLine(cursor, kvp.Item1.Extent))
                    {
                        keyValuePairWithCursor = kvp;
                        break;
                    }

                    //
                    // If cursor is not within a string, then handle following two cases,
                    //
                    //  #1) cursor is after '=', in the same line of previous key value pair
                    //      configuration test{File testfile{DestinationPath='c:\test'; Ensure = |
                    //
                    //  #2) cursor is after '=', in the separate line of previous key value pair
                    //      configuration test{File testfile{DestinationPath='c:\test';
                    //        Ensure = |
                    //
                    if (!IsCursorBeforeExtent(cursor, kvp.Item1.Extent) &&
                        IsCursorAfterExtentAndInTheSameLine(cursor, kvp.Item2.Extent))
                    {
                        keyValuePairWithCursor = kvp;
                    }
                }
            }

            return keyValuePairWithCursor;
        }

        // Pulls the variable out of an assignment's LHS expression
        // Also brings back the innermost type constraint if there is one
        private static VariableExpressionAst GetVariableFromExpressionAst(
            ExpressionAst expression,
            ref Type typeConstraint,
            ref ValidateSetAttribute setConstraint)
        {
            switch (expression)
            {
                // $x = ...
                case VariableExpressionAst variableExpression:
                    return variableExpression;

                // [type]$x = ...
                case ConvertExpressionAst convertExpression:
                    typeConstraint = convertExpression.Type.TypeName.GetReflectionType();
                    return GetVariableFromExpressionAst(convertExpression.Child, ref typeConstraint, ref setConstraint);

                // [attribute()][type]$x = ...
                case AttributedExpressionAst attributedExpressionAst:

                    try
                    {
                        setConstraint = attributedExpressionAst.Attribute.GetAttribute() as ValidateSetAttribute;
                    }
                    catch
                    {
                        // Do nothing, just prevent fallout from an unsuccessful attribute conversion
                    }

                    return GetVariableFromExpressionAst(attributedExpressionAst.Child, ref typeConstraint, ref setConstraint);

                // Something else, like `MemberExpressionAst` $a.p = <tab> which isn't currently handled
                default:
                    return null;
            }
        }

        // Gets any type constraints or validateset constraints on a given variable
        private static bool TryGetTypeConstraintOnVariable(
            CompletionContext completionContext,
            string variableName,
            out Type typeConstraint,
            out ValidateSetAttribute setConstraint)
        {
            typeConstraint = null;
            setConstraint = null;

            PSVariable variable = completionContext.ExecutionContext.EngineSessionState.GetVariable(variableName);

            if (variable == null || variable.Attributes.Count == 0)
            {
                return false;
            }

            foreach (Attribute attribute in variable.Attributes)
            {
                if (attribute is ArgumentTypeConverterAttribute typeConverterAttribute)
                {
                    typeConstraint = typeConverterAttribute.TargetType;
                    continue;
                }

                if (attribute is ValidateSetAttribute validateSetAttribute)
                {
                    setConstraint = validateSetAttribute;
                }
            }

            return typeConstraint != null || setConstraint != null;
        }

        private static List<CompletionResult> CompletePropertyAssignment(MemberExpressionAst memberExpression, CompletionContext context)
        {
            if (SafeExprEvaluator.TrySafeEval(memberExpression, context.ExecutionContext, out var evalValue))
            {
                if (evalValue is null)
                {
                    return null;
                }

                Type type = evalValue.GetType();
                if (type.IsEnum)
                {
                    return GetResultForEnum(type, context);
                }
            }
            return null;
        }

        private static bool TryGetCompletionsForVariableAssignment(
            CompletionContext completionContext,
            AssignmentStatementAst assignmentAst,
            out List<CompletionResult> completions)
        {
            bool TryGetResultForEnum(Type typeConstraint, CompletionContext completionContext, out List<CompletionResult> completions)
            {
                completions = null;

                if (typeConstraint != null && typeConstraint.IsEnum)
                {
                    completions = GetResultForEnum(typeConstraint, completionContext);
                    return true;
                }

                return false;
            }

            bool TryGetResultForSet(Type typeConstraint, ValidateSetAttribute setConstraint, CompletionContext completionContext1, out List<CompletionResult> completions)
            {
                completions = null;

                if (setConstraint?.ValidValues != null)
                {
                    completions = GetResultForSet(typeConstraint, setConstraint.ValidValues, completionContext);
                    return true;
                }

                return false;
            }

            if (assignmentAst.Left is MemberExpressionAst member)
            {
                completions = CompletePropertyAssignment(member, completionContext);
                return completions is not null;
            }

            completions = null;

            // Try to get the variable from the assignment, plus any type constraint on it
            Type typeConstraint = null;
            ValidateSetAttribute setConstraint = null;
            VariableExpressionAst variableAst = GetVariableFromExpressionAst(assignmentAst.Left, ref typeConstraint, ref setConstraint);

            if (variableAst == null)
            {
                return false;
            }

            // Assignment constraints override any existing ones, so try them first

            // Check any [ValidateSet()] constraint first since it's likely to be narrow
            if (TryGetResultForSet(typeConstraint, setConstraint, completionContext, out completions))
            {
                return true;
            }

            // Then try to complete for an enum type
            if (TryGetResultForEnum(typeConstraint, completionContext, out completions))
            {
                return true;
            }

            // If the assignment itself was unconstrained, the variable still might be
            if (!TryGetTypeConstraintOnVariable(completionContext, variableAst.VariablePath.UserPath, out typeConstraint, out setConstraint))
            {
                return false;
            }

            // Again try the [ValidateSet()] constraint first
            if (TryGetResultForSet(typeConstraint, setConstraint, completionContext, out completions))
            {
                return true;
            }

            // Then try to complete for an enum type again
            if (TryGetResultForEnum(typeConstraint, completionContext, out completions))
            {
                return true;
            }

            return false;
        }

        private static List<CompletionResult> GetResultForSet(
            Type typeConstraint,
            IList<string> validValues,
            CompletionContext completionContext)
        {
            var allValues = new List<string>();
            foreach (string value in validValues)
            {
                if (typeConstraint != null && (typeConstraint == typeof(string) || typeConstraint.IsEnum))
                {
                    allValues.Add(GetQuotedString(value, completionContext));
                }
                else
                {
                    allValues.Add(value);
                }
            }

            return GetMatchedResults(allValues, completionContext);
        }

        private static List<CompletionResult> GetMatchedResults(
            List<string> allValues,
            CompletionContext completionContext)
        {
            var stringToComplete = string.Empty;
            if (completionContext.TokenAtCursor != null && completionContext.TokenAtCursor.Kind != TokenKind.Equals)
            {
                stringToComplete = completionContext.TokenAtCursor.Text;
            }

            IEnumerable<string> matchedResults = null;

            if (!string.IsNullOrEmpty(stringToComplete))
            {
                string matchString = stringToComplete + "*";
                var wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);

                matchedResults = allValues.Where(r => wildcardPattern.IsMatch(r));
            }
            else
            {
                matchedResults = allValues;
            }

            var result = new List<CompletionResult>();
            foreach (var match in matchedResults)
            {
                result.Add(new CompletionResult(match));
            }

            return result;
        }

        private static string GetQuotedString(
            string value,
            CompletionContext completionContext)
        {
            var stringToComplete = string.Empty;
            if (completionContext.TokenAtCursor != null)
            {
                stringToComplete = completionContext.TokenAtCursor.Text;
            }

            var quote = stringToComplete.StartsWith('"') ? "\"" : "'";
            return quote + value + quote;
        }

        private static List<CompletionResult> GetResultForEnum(
            Type type,
            CompletionContext completionContext)
        {
            var allNames = new List<string>();
            foreach (var name in Enum.GetNames(type))
            {
                allNames.Add(GetQuotedString(name, completionContext));
            }

            allNames.Sort();

            return GetMatchedResults(allNames, completionContext);
        }

        private static List<CompletionResult> GetResultForEnumPropertyValueOfDSCResource(
            CompletionContext completionContext,
            string stringToComplete,
            ref int replacementIndex,
            ref int replacementLength,
            out bool shouldContinue)
        {
            shouldContinue = true;
            bool isCursorInString = completionContext.TokenAtCursor is StringToken;
            List<CompletionResult> result = null;
            var lastAst = completionContext.RelatedAsts.Last();
            Ast lastChildofHashtableAst;
            var hashTableAst = Ast.GetAncestorHashtableAst(lastAst, out lastChildofHashtableAst);
            Diagnostics.Assert(stringToComplete != null, "stringToComplete should never be null");
            // Check if the hashtable within a DynamicKeyword statement
            if (hashTableAst != null)
            {
                var keywordAst = Ast.GetAncestorAst<DynamicKeywordStatementAst>(hashTableAst);
                if (keywordAst != null)
                {
                    IScriptPosition cursor = completionContext.CursorPosition;
                    var keyValuePairWithCursor = GetHashEntryContainsCursor(cursor, hashTableAst, isCursorInString);
                    if (keyValuePairWithCursor != null)
                    {
                        var propertyNameAst = keyValuePairWithCursor.Item1 as StringConstantExpressionAst;
                        if (propertyNameAst != null)
                        {
                            DynamicKeywordProperty property;
                            if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property))
                            {
                                List<string> existingValues = null;
                                WildcardPattern wildcardPattern = null;
                                bool isDependsOnProperty = string.Equals(property.Name, @"DependsOn", StringComparison.OrdinalIgnoreCase);
                                bool hasNewLine = false;
                                string stringQuote = (completionContext.TokenAtCursor is StringExpandableToken) ? "\"" : "'";
                                if ((property.ValueMap != null && property.ValueMap.Count > 0) || isDependsOnProperty)
                                {
                                    shouldContinue = false;
                                    existingValues = new List<string>();
                                    if (string.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase))
                                    {
                                        var arrayAst = Ast.GetAncestorAst<ArrayLiteralAst>(lastAst);
                                        if (arrayAst != null && arrayAst.Elements.Count > 0)
                                        {
                                            foreach (ExpressionAst expression in arrayAst.Elements)
                                            {
                                                //
                                                // stringAst can be null in following case
                                                //      DependsOn='[user]x',|
                                                //
                                                var stringAst = expression as StringConstantExpressionAst;
                                                if (stringAst != null && IsCursorOutsideOfExtent(cursor, expression.Extent))
                                                {
                                                    existingValues.Add(stringAst.Value);
                                                }
                                            }
                                        }
                                    }
                                    //
                                    // Make sure only auto-complete string value in current line
                                    //
                                    stringToComplete = GetFirstLineSubString(stringToComplete, out hasNewLine);
                                    completionContext.WordToComplete = stringToComplete;
                                    replacementLength = completionContext.ReplacementLength = stringToComplete.Length;
                                    //
                                    // Calculate the replacementIndex based on cursor location (relative to the string token)
                                    //
                                    if (completionContext.TokenAtCursor is StringToken)
                                    {
                                        replacementIndex = completionContext.TokenAtCursor.Extent.StartOffset + 1;
                                    }
                                    else
                                    {
                                        replacementIndex = completionContext.CursorPosition.Offset - replacementLength;
                                    }

                                    completionContext.ReplacementIndex = replacementIndex;
                                    string matchString = stringToComplete + "*";
                                    wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
                                    result = new List<CompletionResult>();
                                }

                                Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time");
                                if (property.ValueMap != null && property.ValueMap.Count > 0)
                                {
                                    IEnumerable<string> orderedValues = property.ValueMap.Keys.Order().Where(v => !existingValues.Contains(v, StringComparer.OrdinalIgnoreCase));
                                    var matchedResults = orderedValues.Where(v => wildcardPattern.IsMatch(v));
                                    if (matchedResults == null || !matchedResults.Any())
                                    {
                                        // Fallback to all allowed values
                                        matchedResults = orderedValues;
                                    }

                                    foreach (var value in matchedResults)
                                    {
                                        string completionText = isCursorInString ? value : stringQuote + value + stringQuote;
                                        if (hasNewLine)
                                            completionText += stringQuote;
                                        result.Add(new CompletionResult(
                                            completionText,
                                            value,
                                            CompletionResultType.Text,
                                            value));
                                    }
                                }
                                else if (isDependsOnProperty)
                                {
                                    var configAst = Ast.GetAncestorAst<ConfigurationDefinitionAst>(keywordAst);
                                    if (configAst != null)
                                    {
                                        var namedBlockAst = Ast.GetAncestorAst<NamedBlockAst>(keywordAst);
                                        if (namedBlockAst != null)
                                        {
                                            List<string> allResources = new List<string>();
                                            foreach (var statementAst in namedBlockAst.Statements)
                                            {
                                                var dynamicKeywordAst = statementAst as DynamicKeywordStatementAst;
                                                if (dynamicKeywordAst != null &&
                                                    dynamicKeywordAst != keywordAst &&
                                                    !string.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase))
                                                {
                                                    if (!string.IsNullOrEmpty(dynamicKeywordAst.ElementName))
                                                    {
                                                        StringBuilder sb = new StringBuilder("[", 50);
                                                        sb.Append(dynamicKeywordAst.Keyword.Keyword);
                                                        sb.Append(']');
                                                        sb.Append(dynamicKeywordAst.ElementName);
                                                        var resource = sb.ToString();
                                                        if (!existingValues.Contains(resource, StringComparer.OrdinalIgnoreCase) &&
                                                            !allResources.Contains(resource, StringComparer.OrdinalIgnoreCase))
                                                        {
                                                            allResources.Add(resource);
                                                        }
                                                    }
                                                }
                                            }

                                            var matchedResults = allResources.Where(r => wildcardPattern.IsMatch(r));
                                            if (matchedResults == null || !matchedResults.Any())
                                            {
                                                // Fallback to all allowed values
                                                matchedResults = allResources;
                                            }

                                            foreach (var resource in matchedResults)
                                            {
                                                string completionText = isCursorInString ? resource : stringQuote + resource + stringQuote;
                                                if (hasNewLine)
                                                    completionText += stringQuote;
                                                result.Add(new CompletionResult(
                                                    completionText,
                                                    resource,
                                                    CompletionResultType.Text,
                                                    resource));
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return result;
        }

        private static List<CompletionResult> GetResultForString(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength)
        {
            var lastAst = completionContext.RelatedAsts.Last();
            var expandableString = lastAst as ExpandableStringExpressionAst;
            var constantString = lastAst as StringConstantExpressionAst;
            if (constantString == null && expandableString == null) { return null; }

            string strValue = constantString != null ? constantString.Value : expandableString.Value;

            bool shouldContinue;
            List<CompletionResult> result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue);
            if (!shouldContinue || (result != null && result.Count > 0))
            {
                return result;
            }

            var commandElementAst = lastAst as CommandElementAst;
            string wordToComplete =
                CompletionCompleters.ConcatenateStringPathArguments(commandElementAst, string.Empty, completionContext);

            if (wordToComplete != null)
            {
                completionContext.WordToComplete = wordToComplete;

                // Handle scenarios like this: cd 'c:\windows\win'<tab>
                if (lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst)
                {
                    result = CompletionCompleters.CompleteCommandArgument(completionContext);
                    replacementIndex = completionContext.ReplacementIndex;
                    replacementLength = completionContext.ReplacementLength;
                }
                // Handle scenarios like this: "c:\wind"<tab>. Treat the StringLiteral/StringExpandable as path/command
                else
                {
                    // Handle path/commandname completion for quoted string
                    result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));

                    // Try command name completion only if the text contains '-'
                    if (wordToComplete.Contains('-'))
                    {
                        var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);
                        if (commandNameResult != null && commandNameResult.Count > 0)
                        {
                            result.AddRange(commandNameResult);
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Find the configuration statement contains current cursor.
        /// </summary>
        /// <param name="cursorPosition"></param>
        /// <param name="ast"></param>
        /// <param name="keywordAst"></param>
        /// <returns></returns>
        private static ConfigurationDefinitionAst GetAncestorConfigurationAstAndKeywordAst(
            IScriptPosition cursorPosition,
            Ast ast,
            out DynamicKeywordStatementAst keywordAst)
        {
            ConfigurationDefinitionAst configureAst = Ast.GetAncestorConfigurationDefinitionAstAndDynamicKeywordStatementAst(ast, out keywordAst);
            // Find the configuration statement contains current cursor
            // Note: cursorPosition.Offset < configureAst.Extent.EndOffset means cursor locates inside the configuration
            //       cursorPosition.Offset = configureAst.Extent.EndOffset means cursor locates at the end of the configuration
            while (configureAst != null && cursorPosition.Offset > configureAst.Extent.EndOffset)
            {
                configureAst = Ast.GetAncestorAst<ConfigurationDefinitionAst>(configureAst.Parent);
            }

            return configureAst;
        }

        /// <summary>
        /// Generate auto complete results for identifier within configuration.
        /// Results are generated based on DynamicKeywords matches given identifier.
        /// For example, following "Fi" matches "File", and "Us" matches "User"
        ///
        ///     Configuration
        ///     {
        ///         Fi^
        ///         Node("TargetMachine")
        ///         {
        ///             Us^
        ///         }
        ///     }
        /// </summary>
        /// <param name="completionContext"></param>
        /// <param name="configureAst"></param>
        /// <param name="keywordAst"></param>
        /// <param name="matched"></param>
        /// <returns></returns>
        private static List<CompletionResult> GetResultForIdentifierInConfiguration(
            CompletionContext completionContext,
            ConfigurationDefinitionAst configureAst,
            DynamicKeywordStatementAst keywordAst,
            out bool matched)
        {
            List<CompletionResult> results = null;
            matched = false;

            IEnumerable<DynamicKeyword> keywords = configureAst.DefinedKeywords.Where(
                k => // Node is special case, legal in both Resource and Meta configuration
                    string.Equals(k.Keyword, @"Node", StringComparison.OrdinalIgnoreCase) ||
                    (
                        // Check compatibility between Resource and Configuration Type
                        k.IsCompatibleWithConfigurationType(configureAst.ConfigurationType) &&
                        !DynamicKeyword.IsHiddenKeyword(k.Keyword) &&
                        !k.IsReservedKeyword
                    )
            );

            if (keywordAst != null && completionContext.CursorPosition.Offset < keywordAst.Extent.EndOffset)
                keywords = keywordAst.Keyword.GetAllowedKeywords(keywords);

            if (keywords != null && keywords.Any())
            {
                string commandName = (completionContext.WordToComplete ?? string.Empty) + "*";
                var wildcardPattern = WildcardPattern.Get(commandName, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);

                // Filter by name
                var matchedResults = keywords.Where(k => wildcardPattern.IsMatch(k.Keyword));
                if (matchedResults == null || !matchedResults.Any())
                {
                    // Fallback to all legal keywords in the configuration statement
                    matchedResults = keywords;
                }
                else
                {
                    matched = true;
                }

                foreach (var keyword in matchedResults)
                {
                    string usageString = string.Empty;
                    ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem<ICrossPlatformDsc>();
                    if (dscSubsystem != null)
                    {
                        usageString = dscSubsystem.GetDSCResourceUsageString(keyword);
                    }
                    else
                    {
                        usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword);
                    }

                    results ??= new List<CompletionResult>();

                    results.Add(new CompletionResult(
                        keyword.Keyword,
                        keyword.Keyword,
                        CompletionResultType.DynamicKeyword,
                        usageString));
                }
            }

            return results;
        }

        private static List<CompletionResult> GetResultForIdentifier(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength)
        {
            List<CompletionResult> result = null;
            var tokenAtCursor = completionContext.TokenAtCursor;
            var lastAst = completionContext.RelatedAsts.Last();

            var tokenAtCursorText = tokenAtCursor.Text;
            completionContext.WordToComplete = tokenAtCursorText;

            if (lastAst.Parent is BreakStatementAst || lastAst.Parent is ContinueStatementAst)
            {
                return CompleteLoopLabel(completionContext);
            }

            var strConst = lastAst as StringConstantExpressionAst;
            if (strConst != null)
            {
                if (strConst.Value.Equals("$", StringComparison.Ordinal))
                {
                    completionContext.WordToComplete = string.Empty;
                    return CompletionCompleters.CompleteVariable(completionContext);
                }
                else
                {
                    UsingStatementAst usingState = strConst.Parent as UsingStatementAst;
                    if (usingState != null)
                    {
                        completionContext.ReplacementIndex = strConst.Extent.StartOffset;
                        completionContext.ReplacementLength = strConst.Extent.EndOffset - replacementIndex;
                        completionContext.WordToComplete = strConst.Extent.Text;
                        switch (usingState.UsingStatementKind)
                        {
                            case UsingStatementKind.Assembly:
                                break;
                            case UsingStatementKind.Command:
                                break;
                            case UsingStatementKind.Module:
                                var moduleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                                {
                                    StringLiterals.PowerShellModuleFileExtension,
                                    StringLiterals.PowerShellDataFileExtension,
                                    StringLiterals.PowerShellNgenAssemblyExtension,
                                    StringLiterals.PowerShellILAssemblyExtension,
                                    StringLiterals.PowerShellILExecutableExtension,
                                    StringLiterals.PowerShellCmdletizationFileExtension
                                };
                                result = CompletionCompleters.CompleteFilename(completionContext, false, moduleExtensions).ToList();
                                if (completionContext.WordToComplete.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1)
                                {
                                    // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath
                                    return result;
                                }

                                var moduleResults = CompletionCompleters.CompleteModuleName(completionContext, false);
                                if (moduleResults != null && moduleResults.Count > 0)
                                    result.AddRange(moduleResults);
                                return result;
                            case UsingStatementKind.Namespace:
                                result = CompletionCompleters.CompleteNamespace(completionContext);
                                return result;
                            case UsingStatementKind.Type:
                                break;
                            default:
                                throw new ArgumentOutOfRangeException("UsingStatementKind");
                        }
                    }
                }
            }
            if (completionContext.TokenAtCursor.TokenFlags == TokenFlags.MemberName && (lastAst is NamedAttributeArgumentAst || lastAst.Parent is NamedAttributeArgumentAst))
            {
                result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
                if (result is not null)
                {
                    return result;
                }
            }

            if ((tokenAtCursor.TokenFlags & TokenFlags.CommandName) != 0)
            {
                // Handle completion for a path with variable, such as: $PSHOME\ty<tab>
                if (completionContext.RelatedAsts.Count > 0 && completionContext.RelatedAsts[0] is ScriptBlockAst)
                {
                    Ast cursorAst = completionContext.RelatedAsts[0].FindAll(
                        ast => ast.Extent.EndOffset <= tokenAtCursor.Extent.StartOffset
                            && ast.Extent is not EmptyScriptExtent,
                        searchNestedScriptBlocks: true).LastOrDefault();

                    if (cursorAst is not null)
                    {
                        if (cursorAst.Extent.EndOffset == tokenAtCursor.Extent.StartOffset)
                        {
                            if (tokenAtCursorText.AsSpan().IndexOfAny('\\', '/') == 0)
                            {
                                string wordToComplete =
                                    CompletionCompleters.ConcatenateStringPathArguments(cursorAst as CommandElementAst, tokenAtCursorText, completionContext);
                                if (wordToComplete != null)
                                {
                                    completionContext.WordToComplete = wordToComplete;
                                    result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
                                    if (result.Count > 0)
                                    {
                                        replacementIndex = cursorAst.Extent.StartScriptPosition.Offset;
                                        replacementLength += cursorAst.Extent.Text.Length;
                                    }

                                    return result;
                                }
                                else
                                {
                                    var variableAst = cursorAst as VariableExpressionAst;
                                    string fullPath = variableAst != null
                                        ? CompletionCompleters.CombineVariableWithPartialPath(
                                            variableAst: variableAst,
                                            extraText: tokenAtCursorText,
                                            executionContext: completionContext.ExecutionContext)
                                        : null;

                                    if (fullPath == null) { return result; }

                                    // Continue trying the filename/commandname completion for scenarios like this: $aa\d<tab>
                                    completionContext.WordToComplete = fullPath;
                                    replacementIndex = cursorAst.Extent.StartScriptPosition.Offset;
                                    replacementLength += cursorAst.Extent.Text.Length;

                                    completionContext.ReplacementIndex = replacementIndex;
                                    completionContext.ReplacementLength = replacementLength;
                                }
                            }
                            // Continue trying the filename/commandname completion for scenarios like this: $aa[get-<tab>
                            else if (cursorAst is not ErrorExpressionAst || cursorAst.Parent is not IndexExpressionAst)
                            {
                                return result;
                            }
                        }

                        if (cursorAst.Parent is IndexExpressionAst indexExpression && indexExpression.Index is ErrorExpressionAst)
                        {
                            if (completionContext.WordToComplete.EndsWith(']'))
                            {
                                completionContext.WordToComplete = completionContext.WordToComplete.Remove(completionContext.WordToComplete.Length - 1);
                            }

                            // Handles index expression with unquoted word like: $PSVersionTable[psver<Tab>]
                            return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
                        }
                    }
                }

                // Handle the StringExpandableToken;
                var strToken = tokenAtCursor as StringExpandableToken;
                if (strToken != null && strToken.NestedTokens != null && strConst != null)
                {
                    try
                    {
                        string expandedString = null;
                        var expandableStringAst = new ExpandableStringExpressionAst(strConst.Extent, strConst.Value, StringConstantType.BareWord);
                        if (CompletionCompleters.IsPathSafelyExpandable(expandableStringAst: expandableStringAst,
                                                                        extraText: string.Empty,
                                                                        executionContext: completionContext.ExecutionContext,
                                                                        expandedString: out expandedString))
                        {
                            completionContext.WordToComplete = expandedString;
                        }
                        else
                        {
                            return result;
                        }
                    }
                    catch (Exception)
                    {
                        return result;
                    }
                }

                //
                // Handle completion of DSC resources within Configuration
                //
                DynamicKeywordStatementAst keywordAst;
                ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst(completionContext.CursorPosition, lastAst, out keywordAst);
                bool matched = false;
                List<CompletionResult> keywordResult = null;
                if (configureAst != null)
                {
                    // Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst
                    keywordResult = GetResultForIdentifierInConfiguration(completionContext, configureAst, keywordAst, out matched);
                }

                // Handle the file completion before command name completion
                result = CompleteFileNameAsCommand(completionContext);
                // Handle the command name completion
                var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);

                if (commandNameResult != null && commandNameResult.Count > 0)
                {
                    result.AddRange(commandNameResult);
                }

                if (matched && keywordResult != null)
                {
                    result.InsertRange(0, keywordResult);
                }
                else if (!matched && keywordResult != null && commandNameResult.Count == 0)
                {
                    result.AddRange(keywordResult);
                }

                return result;
            }

            var isSingleDash = tokenAtCursorText.Length == 1 && tokenAtCursorText[0].IsDash();
            var isDoubleDash = tokenAtCursorText.Length == 2 && tokenAtCursorText[0].IsDash() && tokenAtCursorText[1].IsDash();
            var isParentCommandOrDynamicKeyword = (lastAst.Parent is CommandAst || lastAst.Parent is DynamicKeywordStatementAst);
            if ((isSingleDash || isDoubleDash) && isParentCommandOrDynamicKeyword)
            {
                // When it's the content of a quoted string, we only handle variable/member completion
                if (isSingleDash)
                {
                    var res = CompletionCompleters.CompleteCommandParameter(completionContext);
                    if (res.Count != 0)
                    {
                        return res;
                    }
                }

                return CompletionCompleters.CompleteCommandArgument(completionContext);
            }

            TokenKind memberOperator = TokenKind.Unknown;
            bool isMemberCompletion = lastAst.Parent is MemberExpressionAst;
            bool isStatic = false;
            if (isMemberCompletion)
            {
                var currentExpression = (MemberExpressionAst)lastAst.Parent;
                // Handles following scenario with an incomplete member access token at the end of the statement:
                // [System.IO.FileInfo]::new<Tab>().Directory.BaseName.Length.
                // Traverses up the expressions until it finds one under at the cursor
                while (currentExpression.Extent.EndOffset >= completionContext.CursorPosition.Offset
                    && currentExpression.Expression is MemberExpressionAst memberExpression
                    && memberExpression.Member.Extent.EndOffset >= completionContext.CursorPosition.Offset)
                {
                    currentExpression = memberExpression;
                }

                isStatic = currentExpression.Static;
            }

            bool isWildcard = false;

            if (!isMemberCompletion)
            {
                // Still might be member completion, something like: echo $member.
                // We need to know if the previous element before the token is adjacent because
                // we don't have a MemberExpressionAst, we might have 2 command arguments.

                if (tokenAtCursorText.Equals(TokenKind.Dot.Text(), StringComparison.Ordinal))
                {
                    memberOperator = TokenKind.Dot;
                    isMemberCompletion = true;
                }
                else if (tokenAtCursorText.Equals(TokenKind.ColonColon.Text(), StringComparison.Ordinal))
                {
                    memberOperator = TokenKind.ColonColon;
                    isMemberCompletion = true;
                }
                else if (tokenAtCursor.Kind.Equals(TokenKind.Multiply) && lastAst is BinaryExpressionAst)
                {
                    // Handle member completion with wildcard(wildcard is at the end): $a.p*
                    var binaryExpressionAst = (BinaryExpressionAst)lastAst;
                    var memberExpressionAst = binaryExpressionAst.Left as MemberExpressionAst;
                    var errorPosition = binaryExpressionAst.ErrorPosition;

                    if (memberExpressionAst != null && binaryExpressionAst.Operator == TokenKind.Multiply &&
                        errorPosition.StartOffset == memberExpressionAst.Member.Extent.EndOffset)
                    {
                        isStatic = memberExpressionAst.Static;
                        memberOperator = isStatic ? TokenKind.ColonColon : TokenKind.Dot;
                        isMemberCompletion = true;
                        isWildcard = true;

                        // Member completion will add back the '*', so pretend it wasn't there, at least from the "related asts" point of view,
                        // but add the member expression that we are really completing.
                        completionContext.RelatedAsts.Remove(binaryExpressionAst);
                        completionContext.RelatedAsts.Add(memberExpressionAst);

                        var memberAst = memberExpressionAst.Member as StringConstantExpressionAst;
                        if (memberAst != null)
                        {
                            replacementIndex = memberAst.Extent.StartScriptPosition.Offset;
                            replacementLength += memberAst.Extent.Text.Length;
                        }
                    }
                }
            }

            if (isMemberCompletion)
            {
                result = CompletionCompleters.CompleteMember(completionContext, @static: (isStatic || memberOperator == TokenKind.ColonColon), ref replacementLength);

                // If the last token was just a '.', we tried to complete members.  That may
                // have failed because it wasn't really an attempt to complete a member, in
                // which case we should try to complete as an argument.
                if (result.Count > 0)
                {
                    if (!isWildcard && memberOperator != TokenKind.Unknown)
                    {
                        replacementIndex += tokenAtCursorText.Length;
                        replacementLength = 0;
                    }

                    return result;
                }
            }

            if (lastAst.Parent is HashtableAst)
            {
                result = CompletionCompleters.CompleteHashtableKey(completionContext, (HashtableAst)lastAst.Parent);
                if (result != null && result.Count > 0)
                {
                    return result;
                }
            }

            bool needFileCompletion = false;
            if (lastAst.Parent is FileRedirectionAst || CompleteAgainstSwitchFile(lastAst, completionContext.TokenBeforeCursor))
            {
                string wordToComplete =
                    CompletionCompleters.ConcatenateStringPathArguments(lastAst as CommandElementAst, string.Empty, completionContext);
                if (wordToComplete != null)
                {
                    needFileCompletion = true;
                    completionContext.WordToComplete = wordToComplete;
                }
            }
            else if (tokenAtCursorText.AsSpan().IndexOfAny('\\', '/') == 0)
            {
                var command = lastAst.Parent as CommandBaseAst;
                if (command != null && command.Redirections.Count > 0)
                {
                    var fileRedirection = command.Redirections[0] as FileRedirectionAst;
                    if (fileRedirection != null &&
                        fileRedirection.Extent.EndLineNumber == lastAst.Extent.StartLineNumber &&
                        fileRedirection.Extent.EndColumnNumber == lastAst.Extent.StartColumnNumber)
                    {
                        string wordToComplete =
                            CompletionCompleters.ConcatenateStringPathArguments(fileRedirection.Location, tokenAtCursorText, completionContext);

                        if (wordToComplete != null)
                        {
                            needFileCompletion = true;
                            completionContext.WordToComplete = wordToComplete;
                            replacementIndex = fileRedirection.Location.Extent.StartScriptPosition.Offset;
                            replacementLength += fileRedirection.Location.Extent.EndScriptPosition.Offset - replacementIndex;

                            completionContext.ReplacementIndex = replacementIndex;
                            completionContext.ReplacementLength = replacementLength;
                        }
                    }
                }
            }

            if (needFileCompletion)
            {
                return new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
            }
            else
            {
                string wordToComplete =
                    CompletionCompleters.ConcatenateStringPathArguments(lastAst as CommandElementAst, string.Empty, completionContext);
                if (wordToComplete != null)
                {
                    completionContext.WordToComplete = wordToComplete;
                }
            }

            result = CompletionCompleters.CompleteCommandArgument(completionContext);
            replacementIndex = completionContext.ReplacementIndex;
            replacementLength = completionContext.ReplacementLength;

            return result;
        }

        private static List<CompletionResult> GetResultForAttributeArgument(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength)
        {
            // Attribute member arguments
            Type attributeType = null;
            string argName = string.Empty;
            Ast argAst = completionContext.RelatedAsts.Find(static ast => ast is NamedAttributeArgumentAst);
            AttributeAst attAst;
            if (argAst is NamedAttributeArgumentAst namedArgAst)
            {
                attAst = (AttributeAst)namedArgAst.Parent;
                attributeType = attAst.TypeName.GetReflectionAttributeType();
                argName = namedArgAst.ArgumentName;
                replacementIndex = namedArgAst.Extent.StartOffset;
                replacementLength = argName.Length;
            }
            else
            {
                Ast astAtt = completionContext.RelatedAsts.Find(static ast => ast is AttributeAst);
                attAst = astAtt as AttributeAst;
                if (attAst is not null)
                {
                    attributeType = attAst.TypeName.GetReflectionAttributeType();
                }
            }

            if (attributeType is not null)
            {
                int cursorPosition = completionContext.CursorPosition.Offset;
                var existingArguments = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                foreach (var namedArgument in attAst.NamedArguments)
                {
                    if (cursorPosition < namedArgument.Extent.StartOffset || cursorPosition > namedArgument.Extent.EndOffset)
                    {
                        existingArguments.Add(namedArgument.ArgumentName);
                    }
                }

                PropertyInfo[] propertyInfos = attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                List<CompletionResult> result = new List<CompletionResult>();
                foreach (PropertyInfo property in propertyInfos)
                {
                    // Ignore getter-only properties and properties that have already been set.
                    if (!property.CanWrite || existingArguments.Contains(property.Name))
                    {
                        continue;
                    }

                    if (property.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))
                    {
                        result.Add(new CompletionResult(property.Name, property.Name, CompletionResultType.Property,
                            property.PropertyType.ToString() + " " + property.Name));
                    }
                }

                return result;
            }

            return null;
        }

        /// <summary>
        /// Complete file name as command.
        /// </summary>
        /// <param name="completionContext"></param>
        /// <returns></returns>
        private static List<CompletionResult> CompleteFileNameAsCommand(CompletionContext completionContext)
        {
            var addAmpersandIfNecessary = CompletionCompleters.IsAmpersandNeeded(completionContext, true);
            var result = new List<CompletionResult>();
            var clearLiteralPathsKey = false;

            if (completionContext.Options == null)
            {
                completionContext.Options = new Hashtable { { "LiteralPaths", true } };
            }
            else if (!completionContext.Options.ContainsKey("LiteralPaths"))
            {
                // Dont escape '[',']','`' when the file name is treated as command name
                completionContext.Options.Add("LiteralPaths", true);
                clearLiteralPathsKey = true;
            }

            try
            {
                var fileNameResult = CompletionCompleters.CompleteFilename(completionContext);
                foreach (var entry in fileNameResult)
                {
                    // Add '&' to file names that are quoted
                    var completionText = entry.CompletionText;
                    var len = completionText.Length;
                    if (addAmpersandIfNecessary && len > 2 && completionText[0].IsSingleQuote() && completionText[len - 1].IsSingleQuote())
                    {
                        completionText = "& " + completionText;
                        result.Add(new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip));
                    }
                    else
                    {
                        result.Add(entry);
                    }
                }
            }
            finally
            {
                if (clearLiteralPathsKey)
                    completionContext.Options.Remove("LiteralPaths");
            }

            return result;
        }

        /// <summary>
        /// Complete loop labels after labeled control flow statements such as Break and Continue.
        /// </summary>
        private static List<CompletionResult> CompleteLoopLabel(CompletionContext completionContext)
        {
            var result = new List<CompletionResult>();
            foreach (Ast ast in completionContext.RelatedAsts)
            {
                if (ast is LabeledStatementAst labeledStatement
                    && labeledStatement.Label is not null
                    && (completionContext.WordToComplete is null || labeledStatement.Label.StartsWith(completionContext.WordToComplete, StringComparison.OrdinalIgnoreCase)))
                {
                    result.Add(new CompletionResult(labeledStatement.Label, labeledStatement.Label, CompletionResultType.Text, labeledStatement.Extent.Text));
                }
                else if (ast is ErrorStatementAst errorStatement)
                {
                    // Handles incomplete do/switch loops (other labeled statements do not need this special treatment)
                    // The regex looks for the loopLabel of errorstatements that look like do/switch loops
                    // For example in ":Label do " it will find "Label".
                    var labelMatch = Regex.Match(errorStatement.Extent.Text, @"(?<=^:)\w+(?=\s+(do|switch)\b(?!-))", RegexOptions.IgnoreCase);
                    if (labelMatch.Success)
                    {
                        result.Add(new CompletionResult(labelMatch.Value, labelMatch.Value, CompletionResultType.Text, errorStatement.Extent.Text));
                    }
                }
            }

            if (result.Count == 0)
            {
                return null;
            }

            return result;
        }
      
        private static List<CompletionResult> CompleteUsingKeywords(int cursorOffset, Token[] tokens, ref int replacementIndex, ref int replacementLength)
        {
            var result = new List<CompletionResult>();
            Token tokenBeforeCursor = null;
            Token tokenAtCursor = null;

            for (int i = tokens.Length - 1; i >= 0; i--)
            {
                if (tokens[i].Extent.EndOffset < cursorOffset && tokens[i].Kind != TokenKind.LineContinuation)
                {
                    tokenBeforeCursor = tokens[i];
                    break;
                }
                else if (tokens[i].Extent.StartOffset <= cursorOffset && tokens[i].Extent.EndOffset >= cursorOffset && tokens[i].Kind != TokenKind.LineContinuation)
                {
                    tokenAtCursor = tokens[i];
                }
            }

            if (tokenBeforeCursor is not null && tokenBeforeCursor.Kind == TokenKind.Using)
            {
                string wordToComplete = null;
                if (tokenAtCursor is not null)
                {
                    replacementIndex = tokenAtCursor.Extent.StartOffset;
                    replacementLength = tokenAtCursor.Extent.Text.Length;
                    wordToComplete = tokenAtCursor.Text;
                }
                else
                {
                    replacementIndex = cursorOffset;
                    replacementLength = 0;
                }

                foreach (var keyword in s_usingKeywords)
                {
                    if (string.IsNullOrEmpty(wordToComplete) || keyword.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase))
                    {
                        result.Add(new CompletionResult(keyword, keyword, CompletionResultType.Keyword, GetUsingKeywordToolTip(keyword)));
                    }
                }
            }

            if (result.Count > 0)
            {
                return result;
            }

            return null;
        }

        private static string GetUsingKeywordToolTip(string keyword)
        {
            switch (keyword)
            {
                case "assembly":
                    return TabCompletionStrings.AssemblyKeywordDescription;
                case "module":
                    return TabCompletionStrings.ModuleKeywordDescription;
                case "namespace":
                    return TabCompletionStrings.NamespaceKeywordDescription;
                case "type":
                    return TabCompletionStrings.TypeKeywordDescription;
                default:
                    return null;
            }
        }

        private static readonly string[] s_usingKeywords = new string[]
        {
            "assembly",
            "module",
            "namespace",
            "type"
        };
    }
}
