// Copyright (c) 2022, 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/analysis/features.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_provider.dart';
import 'package:analyzer/src/dart/resolver/record_literal_resolver.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/diagnostic/diagnostic_factory.dart';
import 'package:analyzer/src/error/listener.dart';

/// Helper for resolving [RecordTypeAnnotation]s.
class RecordTypeAnnotationResolver {
  final TypeProviderImpl typeProvider;
  final DiagnosticReporter _diagnosticReporter;
  final LibraryElement libraryElement;

  RecordTypeAnnotationResolver({
    required this.typeProvider,
    required DiagnosticReporter diagnosticReporter,
    required this.libraryElement,
  }) : _diagnosticReporter = diagnosticReporter;

  bool get isWildCardVariablesEnabled =>
      libraryElement.featureSet.isEnabled(Feature.wildcard_variables);

  bool isPositionalWildCard(AstNode field, String name) =>
      field is RecordTypeAnnotationPositionalField &&
      name == '_' &&
      isWildCardVariablesEnabled;

  /// Report any named fields in the record type [node] that use a previously
  /// defined name.
  void reportDuplicateFieldDefinitions(RecordTypeAnnotationImpl node) {
    var usedNames = <String, RecordTypeAnnotationField>{};
    for (var field in node.fields) {
      var name = field.name?.lexeme;
      if (name != null) {
        // Multiple positional `_`s are legal with wildcards.
        if (isPositionalWildCard(field, name)) continue;

        var previousField = usedNames[name];
        if (previousField != null) {
          _diagnosticReporter.report(
            DiagnosticFactory().duplicateFieldDefinitionInType(
              _diagnosticReporter.source,
              field,
              previousField,
            ),
          );
        } else {
          usedNames[name] = field;
        }
      }
    }
  }

  /// Report any fields in the record type [node] that use an invalid name.
  void reportInvalidFieldNames(RecordTypeAnnotationImpl node) {
    var positionalFields = node.positionalFields;
    var positionalCount = positionalFields.length;
    for (var field in node.fields) {
      var nameToken = field.name;
      if (nameToken != null) {
        var name = nameToken.lexeme;
        if (name.startsWith('_')) {
          // Positional record fields named `_` are legal w/ wildcards.
          if (!isPositionalWildCard(field, name)) {
            _diagnosticReporter.report(
              diag.invalidFieldNamePrivate.at(nameToken),
            );
          }
        } else {
          var index = RecordTypeExtension.positionalFieldIndex(name);
          if (index != null) {
            if (index < positionalCount &&
                positionalFields.indexOf(field) != index) {
              _diagnosticReporter.report(
                diag.invalidFieldNamePositional.at(nameToken),
              );
            }
          } else if (RecordLiteralResolver.isForbiddenNameForRecordField(
            name,
          )) {
            _diagnosticReporter.report(
              diag.invalidFieldNameFromObject.at(nameToken),
            );
          }
        }
      }
    }
  }

  void resolve(RecordTypeAnnotationImpl node) {
    _buildType(node);
    reportDuplicateFieldDefinitions(node);
    reportInvalidFieldNames(node);
  }

  void _buildType(RecordTypeAnnotationImpl node) {
    var positionalFields = node.positionalFields.map((field) {
      return RecordTypePositionalFieldImpl(type: field.type.typeOrThrow);
    }).toList();

    var namedFields = node.namedFields?.fields.map((field) {
      return RecordTypeNamedFieldImpl(
        name: field.name.lexeme,
        type: field.type.typeOrThrow,
      );
    }).toList();

    node.type = RecordTypeImpl(
      positionalFields: positionalFields,
      namedFields: namedFields ?? const [],
      nullabilitySuffix: node.question != null
          ? NullabilitySuffix.question
          : NullabilitySuffix.none,
    );
  }
}
