// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/error/codes.dart';

/// Checks if the arguments for a parameter annotated with `@mustBeConst` are
/// actually constant.
class ConstArgumentsVerifier extends SimpleAstVisitor<void> {
  final ErrorReporter _errorReporter;

  ConstArgumentsVerifier(this._errorReporter);

  @override
  void visitAssignmentExpression(AssignmentExpression node) {
    if (node.operator.type == TokenType.EQ) {
      _check(
        arguments: [node.rightHandSide],
        errorNode: node.operator,
      );
    } else if (node
            .rightHandSide.correspondingParameter?.metadata2.hasMustBeConst ??
        false) {
      // If the operator is not `=`, then the argument cannot be const, as it
      // depends on the value of the left hand side.
      _errorReporter.atNode(
        node.rightHandSide,
        WarningCode.NON_CONST_ARGUMENT_FOR_CONST_PARAMETER,
        arguments: [node.rightHandSide],
      );
    }
  }

  @override
  void visitBinaryExpression(BinaryExpression node) {
    _check(
      arguments: [node.rightOperand],
      errorNode: node.operator,
    );
  }

  @override
  void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
    if (node.staticInvokeType is FunctionType) {
      _check(
        arguments: node.argumentList.arguments,
        errorNode: node,
      );
    }
  }

  @override
  void visitIndexExpression(IndexExpression node) {
    _check(
      arguments: [node.index],
      errorNode: node.leftBracket,
    );
  }

  @override
  void visitInstanceCreationExpression(InstanceCreationExpression node) {
    if (node.inConstantContext) return;
    _check(
      arguments: node.argumentList.arguments,
      errorNode: node.constructorName,
    );
  }

  @override
  void visitMethodInvocation(MethodInvocation node) {
    _check(
      arguments: node.argumentList.arguments,
      errorNode: node.methodName,
    );
  }

  @override
  void visitRedirectingConstructorInvocation(
      RedirectingConstructorInvocation node) {
    _check(
      arguments: node.argumentList.arguments,
      errorNode: node.constructorName ?? node.thisKeyword,
    );
  }

  @override
  void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
    _check(
      arguments: node.argumentList.arguments,
      errorNode: node.constructorName ?? node.superKeyword,
    );
  }

  void _check({
    required List<Expression> arguments,
    required SyntacticEntity errorNode,
  }) {
    for (var argument in arguments) {
      var parameter = argument.correspondingParameter;
      if (parameter == null) {
        continue;
      }

      var parameterName = parameter.name3;
      if (parameterName == null) {
        continue;
      }

      if (parameter.metadata2.hasMustBeConst) {
        Expression resolvedArgument;
        if (parameter.isNamed) {
          resolvedArgument = (argument as NamedExpression).expression;
        } else {
          resolvedArgument = argument;
        }
        if (!_isConst(resolvedArgument)) {
          _errorReporter.atNode(
            argument,
            WarningCode.NON_CONST_ARGUMENT_FOR_CONST_PARAMETER,
            arguments: [parameterName],
          );
        }
      }
    }
  }

  bool _isConst(Expression expression) {
    if (expression.inConstantContext) {
      return true;
    } else if (expression is InstanceCreationExpression && expression.isConst) {
      return true;
    } else if (expression is Literal) {
      return switch (expression) {
        BooleanLiteral() => true,
        DoubleLiteral() => true,
        IntegerLiteral() => true,
        NullLiteral() => true,
        SimpleStringLiteral() => true,
        AdjacentStrings() => true,
        SymbolLiteral() => true,
        RecordLiteral() => expression.isConst,
        TypedLiteral() => expression.isConst,
        // TODO(mosum): Expand the logic to check if the individual interpolation elements are const.
        StringInterpolation() => false,
      };
    } else if (expression is Identifier) {
      var element = expression.element;
      switch (element) {
        case GetterElement(variable3: var variable?):
          return variable.isConst;
        case VariableElement2():
          return element.isConst;
      }
    }
    return false;
  }
}
