context: using Antlr4.Runtime;
  using NLog;
  using Rubberduck.Parsing.Annotations;
  using Rubberduck.Parsing.Binding;
  using Rubberduck.Parsing.Grammar;
  using Rubberduck.Parsing.Symbols.DeclarationLoaders;
  using Rubberduck.VBEditor;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading;
  using Rubberduck.Parsing.VBA.DeclarationCaching;
  using Rubberduck.Parsing.VBA.ReferenceManagement;
  
  namespace Rubberduck.Parsing.Symbols
  {
      public sealed class IdentifierReferenceResolver
      {
          private readonly DeclarationFinder _declarationFinder;
          private readonly QualifiedModuleName _qualifiedModuleName;
          private readonly Stack<IBoundExpression> _withBlockExpressions;
          private readonly Declaration _moduleDeclaration;
          private Declaration _currentScope;
          private Declaration _currentParent;
          private readonly BindingService _bindingService;
          private readonly BoundExpressionVisitor _boundExpressionVisitor;
          private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
  
          public IdentifierReferenceResolver(QualifiedModuleName qualifiedModuleName, DeclarationFinder finder)
          {
              _declarationFinder = finder;
              _qualifiedModuleName = qualifiedModuleName;
              _withBlockExpressions = new Stack<IBoundExpression>();
              _moduleDeclaration = finder.MatchName(_qualifiedModuleName.ComponentName)
                  .SingleOrDefault(item =>
                      (item.DeclarationType.HasFlag(DeclarationType.ClassModule) ||
                       item.DeclarationType == DeclarationType.ProceduralModule)
                      && item.QualifiedName.QualifiedModuleName.Equals(_qualifiedModuleName));
              SetCurrentScope();
              var typeBindingContext = new TypeBindingContext(_declarationFinder);
              var procedurePointerBindingContext = new ProcedurePointerBindingContext(_declarationFinder);
              _bindingService = new BindingService(
                  _declarationFinder,
                  new DefaultBindingContext(_declarationFinder, typeBindingContext, procedurePointerBindingContext),
                  typeBindingContext,
                  procedurePointerBindingContext);
              _boundExpressionVisitor = new BoundExpressionVisitor(finder);
          }
  
          public void SetCurrentScope()
          {
              _currentScope = _moduleDeclaration;
              _currentParent = _moduleDeclaration;
          }
  
          public void SetCurrentScope(string memberName, DeclarationType type)
          {
              Logger.Trace("Setting current scope: {0} ({1}) in thread {2}", memberName, type,
                  Thread.CurrentThread.ManagedThreadId);
  
              _currentParent = _declarationFinder.MatchName(memberName).SingleOrDefault(item =>
                  item.QualifiedName.QualifiedModuleName == _qualifiedModuleName && item.DeclarationType == type);
  
              _currentScope = _declarationFinder.MatchName(memberName).SingleOrDefault(item =>
                  item.QualifiedName.QualifiedModuleName == _qualifiedModuleName && item.DeclarationType == type) ??
                              _moduleDeclaration;
  
              Logger.Trace("Current scope is now {0} in thread {1}",
                  _currentScope == null ? "null" : _currentScope.IdentifierName, Thread.CurrentThread.ManagedThreadId);
          }
  
          public void EnterWithBlock(VBAParser.WithStmtContext context)
          {
              var boundExpression = _bindingService.ResolveDefault(
                  _moduleDeclaration,
                  _currentParent,
                  context.expression(),
                  GetInnerMostWithExpression(),
                  StatementResolutionContext.Undefined);
              _boundExpressionVisitor.AddIdentifierReferences(boundExpression, _qualifiedModuleName, _currentScope, _currentParent);
              // note: pushes null if unresolved
              _withBlockExpressions.Push(boundExpression);
          }
  
          private IBoundExpression GetInnerMostWithExpression()
          {
              if (_withBlockExpressions.Any())
              {
                  return _withBlockExpressions.Peek();
              }
              return null;
          }
  
          public void ExitWithBlock()
          {
              _withBlockExpressions.Pop();
          }
  
          public void Resolve(VBAParser.ArgDefaultValueContext context)
          {
              var expression = context.expression();
              if (expression == null)
              {
                  return;
              }
              ResolveDefault(expression);
          }
  
          public void Resolve(VBAParser.ArrayDimContext context)
          {
              if (context.boundsList() == null)
              {
                  return;
              }
              foreach (var dimSpec in context.boundsList().dimSpec())
              {
                  if (dimSpec.lowerBound() != null)
                  {
                      ResolveDefault(dimSpec.lowerBound().constantExpression().expression());
                  }
                  ResolveDefault(dimSpec.upperBound().constantExpression().expression());
              }
          }
  
          public void Resolve(VBAParser.OnErrorStmtContext context)
          {
              if (context.expression() == null)
              {
                  return;
              }
              ResolveLabel(context.expression(), context.expression().GetText());
          }
  
          public void Resolve(VBAParser.ErrorStmtContext context)
          {
              ResolveDefault(context.expression());
          }
  
          private void ResolveLabel(ParserRuleContext context, string label)
          {
              var labelDeclaration = _bindingService.ResolveGoTo(_currentParent, label);
              if (labelDeclaration != null)
              {
                  var callSiteContext = context;
                  var identifier = context.GetText();
                  var callee = labelDeclaration;
                  labelDeclaration.AddReference(
                      _qualifiedModuleName,
                      _currentScope,
                      _currentParent,
                      callSiteContext,
                      identifier,
                      callee,
                      callSiteContext.GetSelection(),
                      FindIdentifierAnnotations(_qualifiedModuleName, callSiteContext.GetSelection().StartLine));
              }
          }
  
          private IEnumerable<IAnnotation> FindIdentifierAnnotations(QualifiedModuleName module, int line)
          {
              return _declarationFinder.FindAnnotations(module, line)
                  .Where(annotation => annotation.AnnotationType.HasFlag(AnnotationType.IdentifierAnnotation));
          }
  
          private void ResolveDefault(
              ParserRuleContext expression,
              StatementResolutionContext statementContext = StatementResolutionContext.Undefined,
              bool isAssignmentTarget = false,
              bool hasExplicitLetStatement = false,
              bool isSetAssignment = false)
          {
              var withExpression = GetInnerMostWithExpression();
              var boundExpression = _bindingService.ResolveDefault(
                  _moduleDeclaration,
                  _currentParent,
                  expression,
                  withExpression,
                  statementContext);
              if (boundExpression.Classification == ExpressionClassification.ResolutionFailed)
              {
                  var lexpression = expression as VBAParser.LExpressionContext
                                      ?? expression.GetChild<VBAParser.LExpressionContext>(0)
                                      ?? (expression as VBAParser.LExprContext
                                          ?? expression.GetChild<VBAParser.LExprContext>(0))?.lExpression();
  
                  if (lexpression != null)
                  {
                      _declarationFinder.AddUnboundContext(_currentParent, lexpression, withExpression);
                  }
                  else
                  {
                      Logger.Warn(
                          $"Default Context: Failed to resolve {expression.GetText()}. Binding as much as we can.");
                  }
              }
  
              IParameterizedDeclaration defaultMember = null;
              if (boundExpression.ReferencedDeclaration != null 
                  && boundExpression.ReferencedDeclaration.DeclarationType != DeclarationType.Project
                  && boundExpression.ReferencedDeclaration.AsTypeDeclaration != null)
              {
                  var module = boundExpression.ReferencedDeclaration.AsTypeDeclaration;
                  var members = _declarationFinder.Members(module);
                  defaultMember = (IParameterizedDeclaration) members.FirstOrDefault(member =>
                      member is IParameterizedDeclaration && member.Attributes.HasDefaultMemberAttribute() 
                          && (isAssignmentTarget
                              ? member.DeclarationType.HasFlag(DeclarationType.Procedure)
                              : member.DeclarationType.HasFlag(DeclarationType.Function)));
              }
  
              _boundExpressionVisitor.AddIdentifierReferences(
                  boundExpression, 
                  _qualifiedModuleName, 
                  _currentScope,
                  _currentParent,
                  isAssignmentTarget && (defaultMember == null || isSetAssignment || defaultMember.Parameters.All(param => param.IsOptional)),
                  hasExplicitLetStatement, 
                  isSetAssignment);
          }
  
          private void ResolveType(ParserRuleContext expression)
          {
              var boundExpression = _bindingService.ResolveType(_moduleDeclaration, _currentParent, expression);
              if (boundExpression.Classification == ExpressionClassification.ResolutionFailed)
              {
                  Logger.Warn($"Type Context: Failed to resolve {expression.GetText()}. Binding as much as we can.");
              }
              _boundExpressionVisitor.AddIdentifierReferences(boundExpression, _qualifiedModuleName, _currentScope, _currentParent);
          }
  
          public void Resolve(VBAParser.GoToStmtContext context)
          {
              ResolveLabel(context.expression(), context.expression().GetText());
          }
  
          public void Resolve(VBAParser.OnGoToStmtContext context)
          {
              ResolveDefault(context.expression()[0]);
              for (int labelIndex = 1; labelIndex < context.expression().Length; labelIndex++)
              {
                  ResolveLabel(context.expression()[labelIndex], context.expression()[labelIndex].GetText());
              }
          }
  
          public void Resolve(VBAParser.GoSubStmtContext context)
          {
              ResolveLabel(context.expression(), context.expression().GetText());
          }
  
          public void Resolve(VBAParser.OnGoSubStmtContext context)
          {
              ResolveDefault(context.expression()[0]);
              for (int labelIndex = 1; labelIndex < context.expression().Length; labelIndex++)
              {
                  ResolveLabel(context.expression()[labelIndex], context.expression()[labelIndex].GetText());
              }
          }
  
          public void Resolve(VBAParser.RedimStmtContext context)
          {
              // TODO: Create local variable if no match for redim variable declaration.
              foreach (var redimVariableDeclaration in context.redimDeclarationList().redimVariableDeclaration())
              {
                  // We treat redim statements as index expressions to make it SLL.
                  var lExpr = ((VBAParser.LExprContext)redimVariableDeclaration.expression()).lExpression();
                  var indexExpr = (VBAParser.IndexExprContext)lExpr;
                  // The lexpression is the array that is being resized.
                  // We can't treat it as a normal index expression because the semantics are different.
                  // It's not actually a function call but a special statement.
                  ResolveDefault(indexExpr.lExpression());
                  var argumentList = indexExpr.argumentList();
                  if (argumentList.argument() != null)
                  {
                      foreach (var positionalArgument in argumentList.argument())
                      {
                          if (positionalArgument.positionalArgument() != null)
                          {
                              ResolveRedimArgument(positionalArgument.positionalArgument().argumentExpression());
                          }
                      }
                  }
              }
          }
  
          private void ResolveRedimArgument(VBAParser.ArgumentExpressionContext argument)
          {
              // Redim statements can either have "normal" positional argument expressions or lower + upper bounds arguments.
              if (argument.lowerBoundArgumentExpression() != null)
              {
                  ResolveDefault(argument.lowerBoundArgumentExpression().expression());
                  ResolveDefault(argument.upperBoundArgumentExpression().expression());
              }
              else
              {
                  ResolveDefault(argument.expression());
              }
          }
  
          public void Resolve(VBAParser.WhileWendStmtContext context)
          {
              ResolveDefault(context.expression());
          }
  
          public void Resolve(VBAParser.DoLoopStmtContext context)
          {
              if (context.expression() == null)
              {
                  return;
              }
              ResolveDefault(context.expression());
          }
  
          public void Resolve(VBAParser.IfStmtContext context)
          {
              ResolveDefault(context.booleanExpression());
              if (context.elseIfBlock() != null)
              {
                  foreach (var elseIfBlock in context.elseIfBlock())
                  {
                      ResolveDefault(elseIfBlock.booleanExpression());
                  }
              }
          }
  
          public void Resolve(VBAParser.SingleLineIfStmtContext context)
          {
              // The listOrLabel rule could be resolved separately but since it's such a special case, only appearing in
              // single-line-if-statements, we do it here for better understanding.
              if (context.ifWithEmptyThen() != null)
              {
                  ResolveDefault(context.ifWithEmptyThen().booleanExpression());
                  ResolveListOrLabel(context.ifWithEmptyThen().singleLineElseClause().listOrLabel());
              }
              else
              {
                  ResolveDefault(context.ifWithNonEmptyThen().booleanExpression());
                  ResolveListOrLabel(context.ifWithNonEmptyThen().listOrLabel());
                  if (context.ifWithNonEmptyThen().singleLineElseClause() != null)
                  {
                      ResolveListOrLabel(context.ifWithNonEmptyThen().singleLineElseClause().listOrLabel());
                  }
              }
          }
  
          private void ResolveListOrLabel(VBAParser.ListOrLabelContext listOrLabel)
          {
              if (listOrLabel?.lineNumberLabel() == null)
              {
                  return;
              }
              ResolveLabel(listOrLabel.lineNumberLabel(), listOrLabel.lineNumberLabel().GetText());
          }
  
          public void Resolve(VBAParser.SelectCaseStmtContext context)
          {
              ResolveDefault(context.selectExpression().expression());
              if (context.caseClause() == null)
              {
                  return;
              }
              foreach (var caseClause in context.caseClause())
              {
                  foreach (var rangeClause in caseClause.rangeClause())
                  {
                      if (rangeClause.expression() != null)
                      {
                          ResolveDefault(rangeClause.expression());
                      }
                      else
                      {
                          ResolveDefault(rangeClause.selectStartValue().expression());
                          ResolveDefault(rangeClause.selectEndValue().expression());
                      }
                  }
              }
          }
  
          public void Resolve(VBAParser.LetStmtContext context)
          {
              var letStatement = context.LET();
              ResolveDefault(
                  context.lExpression(),
                  StatementResolutionContext.LetStatement,
                  true,
                  letStatement != null);
              ResolveDefault(context.expression());
          }
  
          public void Resolve(VBAParser.SetStmtContext context)
          {
              ResolveDefault(
                  context.lExpression(),
                  StatementResolutionContext.SetStatement,
                  true,
                  false,
                  true);
              ResolveDefault(context.expression());
          }
  
          public void Resolve(VBAParser.CallStmtContext context)
          {
              ResolveDefault(context);
          }
  
          public void Resolve(VBAParser.ConstStmtContext context)
          {
              foreach (var constStmt in context.constSubStmt())
              {
                  ResolveDefault(constStmt.expression());
              }
          }
  
          public void Resolve(VBAParser.EraseStmtContext context)
          {
              foreach (var expr in context.expression())
              {
                  ResolveDefault(expr);
              }
          }
  
          public void Resolve(VBAParser.NameStmtContext context)
          {
              foreach (var expr in context.expression())
              {
                  ResolveDefault(expr);
              }
          }
  
          private void ResolveFileNumber(VBAParser.FileNumberContext fileNumber)
          {
              ResolveDefault(fileNumber.markedFileNumber() != null
                  ? fileNumber.markedFileNumber().expression()
                  : fileNumber.unmarkedFileNumber().expression());
          }
  
          public void Resolve(VBAParser.OpenStmtContext context)
          {
              ResolveDefault(context.pathName().expression());
              ResolveFileNumber(context.fileNumber());
              if (context.lenClause() != null)
              {
                  ResolveDefault(context.lenClause().recLength().expression());
              }
          }
  
          public void Resolve(VBAParser.CloseStmtContext context)
          {
              if (context.fileNumberList() != null)
              {
                  foreach (var fileNumber in context.fileNumberList().fileNumber())
                  {
                      ResolveFileNumber(fileNumber);
                  }
              }
          }
  
          public void Resolve(VBAParser.SeekStmtContext context)
          {
              ResolveFileNumber(context.fileNumber());
              ResolveDefault(context.position().expression());
          }
  
          public void Resolve(VBAParser.LockStmtContext context)
          {
              ResolveFileNumber(context.fileNumber());
              if (context.recordRange() == null)
              {
                  return;
              }
              ResolveRecordRange(context.recordRange());
          }
  
          public void Resolve(VBAParser.UnlockStmtContext context)
          {
              ResolveFileNumber(context.fileNumber());
              ResolveRecordRange(context.recordRange());
          }
  
          private void ResolveRecordRange(VBAParser.RecordRangeContext recordRange)
          {
              if (recordRange == null)
              {
                  return;
              }
              if (recordRange.startRecordNumber() != null)
              {
                  ResolveDefault(recordRange.startRecordNumber().expression());
              }
              if (recordRange.endRecordNumber() != null)
              {
                  ResolveDefault(recordRange.endRecordNumber().expression());
              }
          }
  
          public void Resolve(VBAParser.LineInputStmtContext context)
          {
              ResolveDefault(context.markedFileNumber().expression());
              ResolveDefault(context.variableName().expression(), isAssignmentTarget: true);
          }
  
          public void Resolve(VBAParser.WidthStmtContext context)
          {
              ResolveDefault(context.markedFileNumber().expression());
              ResolveDefault(context.lineWidth().expression());
          }
  
          public void Resolve(VBAParser.PrintStmtContext context)
          {
              ResolveDefault(context.markedFileNumber().expression());
              ResolveOutputList(context.outputList());
          }
  
          public void Resolve(VBAParser.WriteStmtContext context)
          {
              ResolveDefault(context.markedFileNumber().expression());
              ResolveOutputList(context.outputList());
          }
  
          private void ResolveOutputList(VBAParser.OutputListContext outputList)
          {
  
gt:             if (outputList == null)