// 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/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/token.dart';
import 'package:analyzer/src/dart/constant/compute.dart';
import 'package:analyzer/src/dart/constant/constant_verifier.dart';
import 'package:analyzer/src/dart/constant/evaluation.dart';
import 'package:analyzer/src/dart/constant/potentially_constant.dart';
import 'package:analyzer/src/dart/constant/utilities.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;

/// A diagnostic listener that only records whether any constant-related
/// diagnostics have been reported.
class _ConstantDiagnosticListener extends DiagnosticListener {
  /// A flag indicating whether any constant-related diagnostics have been
  /// reported to this listener.
  bool hasConstError = false;

  @override
  void onDiagnostic(Diagnostic diagnostic) {
    DiagnosticCode diagnosticCode = diagnostic.diagnosticCode;
    switch (diagnosticCode) {
      case diag.constConstructorConstantFromDeferredLibrary:
      case diag.constConstructorWithFieldInitializedByNonConst:
      case diag.constEvalExtensionMethod:
      case diag.constEvalExtensionTypeMethod:
      case diag.constEvalMethodInvocation:
      case diag.constEvalPropertyAccess:
      case diag.constEvalTypeBool:
      case diag.constEvalTypeBoolInt:
      case diag.constEvalTypeBoolNumString:
      case diag.constEvalTypeInt:
      case diag.constEvalTypeNum:
      case diag.constEvalTypeNumString:
      case diag.constEvalTypeString:
      case diag.constEvalThrowsException:
      case diag.constEvalThrowsIdbze:
      case diag.constEvalForElement:
      case diag.constMapKeyNotPrimitiveEquality:
      case diag.constSetElementNotPrimitiveEquality:
      case diag.constTypeParameter:
      case diag.constWithNonConst:
      case diag.constWithNonConstantArgument:
      case diag.constWithTypeParameters:
      case diag.constWithTypeParametersConstructorTearoff:
      case diag.invalidConstant:
      case diag.missingConstInListLiteral:
      case diag.missingConstInMapLiteral:
      case diag.missingConstInSetLiteral:
      case diag.nonBoolCondition:
      case diag.nonConstantListElement:
      case diag.nonConstantMapElement:
      case diag.nonConstantMapKey:
      case diag.nonConstantMapValue:
      case diag.nonConstantRecordField:
      case diag.nonConstantSetElement:
        hasConstError = true;
    }
  }
}

extension AstNodeExtension on AstNode {
  /// Whether [ConstantVerifier] reports an error when computing the value of
  /// `this` as a constant.
  bool get hasConstantVerifierError {
    var unitNode = thisOrAncestorOfType<CompilationUnitImpl>();
    var unitFragment = unitNode?.declaredFragment;
    if (unitFragment == null) return false;

    var libraryElement = unitFragment.element;
    var declaredVariables = libraryElement.session.declaredVariables;

    var dependenciesFinder = ConstantExpressionsDependenciesFinder();
    accept(dependenciesFinder);
    computeConstants(
      declaredVariables: declaredVariables,
      constants: dependenciesFinder.dependencies.toList(),
      featureSet: libraryElement.featureSet,
      configuration: ConstantEvaluationConfiguration(),
    );

    var listener = _ConstantDiagnosticListener();
    var errorReporter = DiagnosticReporter(listener, unitFragment.source);

    accept(ConstantVerifier(errorReporter, libraryElement, declaredVariables));
    return listener.hasConstError;
  }
}

extension ConstructorDeclarationExtension on ConstructorDeclaration {
  bool get canBeConst {
    var element = declaredFragment!.element;

    var classElement = element.enclosingElement;
    if (classElement is ClassElement && classElement.hasNonFinalField) {
      return false;
    }

    var oldKeyword = constKeyword;
    var self = this as ConstructorDeclarationImpl;
    try {
      temporaryConstConstructorElements[element] = true;
      self.constKeyword = KeywordToken(Keyword.CONST, offset);
      return !hasConstantVerifierError;
    } finally {
      temporaryConstConstructorElements[element] = null;
      self.constKeyword = oldKeyword;
    }
  }
}

extension PrimaryConstructorDeclarationExtension
    on PrimaryConstructorDeclaration {
  bool get canBeConst {
    var element = declaredFragment!.element;

    var classElement = element.enclosingElement;
    if (classElement is ClassElement && classElement.hasNonFinalField) {
      return false;
    }

    var oldKeyword = constKeyword;
    var self = this as PrimaryConstructorDeclarationImpl;
    try {
      temporaryConstConstructorElements[element] = true;
      self.constKeyword = KeywordToken(Keyword.CONST, offset);
      return !hasConstantVerifierError;
    } finally {
      temporaryConstConstructorElements[element] = null;
      self.constKeyword = oldKeyword;
    }
  }
}
