// Copyright (c) 2019, 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/element/element2.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/error/codes.dart';

/// Verifier for initializing fields in constructors.
class ConstructorFieldsVerifier {
  final TypeSystemImpl typeSystem;
  final Map<InstanceElement2, _Interface> _interfaces = Map.identity();

  ConstructorFieldsVerifier({
    required this.typeSystem,
  });

  void addConstructors(
    ErrorReporter errorReporter,
    InterfaceElement2 element,
    List<ClassMember> members,
  ) {
    var interfaceFields = _forInterface(element);
    var constructors = members.whereType<ConstructorDeclarationImpl>();
    for (var constructor in constructors) {
      _addConstructor(
        errorReporter: errorReporter,
        interfaceFields: interfaceFields,
        node: constructor,
      );
    }
  }

  void report() {
    for (var interface in _interfaces.values) {
      for (var constructor in interface.constructors.values) {
        constructor.report();
      }
    }
  }

  void _addConstructor({
    required ErrorReporter errorReporter,
    required _Interface interfaceFields,
    required ConstructorDeclarationImpl node,
  }) {
    if (node.factoryKeyword != null ||
        node.redirectedConstructor != null ||
        node.externalKeyword != null) {
      return;
    }

    var fragment = node.declaredFragment!;
    var constructorState = interfaceFields.forConstructor(
      errorReporter: errorReporter,
      node: node,
      fragment: fragment,
    );

    if (!fragment.isAugmentation) {
      constructorState.updateWithParameters(node);
    }

    constructorState.updateWithInitializers(errorReporter, node);
  }

  _Interface _forInterface(InterfaceElement2 element) {
    if (_interfaces[element] case var result?) {
      return result;
    }

    var fieldMap = <FieldElement2, _InitState>{};

    for (var field in element.fields2) {
      if (field.isSynthetic) {
        continue;
      }
      if (element is EnumElement2 && field.name3 == 'index') {
        continue;
      }
      fieldMap[field] = field.hasInitializer
          ? _InitState.initInDeclaration
          : _InitState.notInit;
    }

    return _interfaces[element] = _Interface(
      typeSystem: typeSystem,
      element: element,
      fields: fieldMap,
    );
  }
}

class _Constructor {
  final TypeSystemImpl typeSystem;
  final ErrorReporter errorReporter;
  final ConstructorDeclaration node;
  final ConstructorElement2 element;
  final Map<FieldElement2, _InitState> fields;

  /// Set to `true` if the constructor redirects.
  bool hasRedirectingConstructorInvocation = false;

  _Constructor({
    required this.typeSystem,
    required this.errorReporter,
    required this.node,
    required this.element,
    required this.fields,
  });

  void report() {
    if (hasRedirectingConstructorInvocation) {
      return;
    }

    // Prepare lists of not initialized fields.
    var notInitFinalFields = <_Field>[];
    var notInitNonNullableFields = <_Field>[];
    fields.forEach((field, state) {
      if (state != _InitState.notInit) return;
      if (field.isLate) return;
      if (field.isAbstract || field.isExternal) return;
      if (field.isStatic) return;

      var name = field.name3;
      if (name == null) return;

      if (field.isFinal) {
        notInitFinalFields.add(
          _Field(field, name),
        );
      } else if (typeSystem.isPotentiallyNonNullable(field.type)) {
        notInitNonNullableFields.add(
          _Field(field, name),
        );
      }
    });

    reportNotInitializedFinal(notInitFinalFields);
    reportNotInitializedNonNullable(notInitNonNullableFields);
  }

  void reportNotInitializedFinal(
    List<_Field> notInitFinalFields,
  ) {
    if (notInitFinalFields.isEmpty) {
      return;
    }

    var names = notInitFinalFields.map((f) => f.name).toList();
    names.sort();

    if (names.length == 1) {
      errorReporter.atNode(
        node.returnType,
        CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_1,
        arguments: names,
      );
    } else if (names.length == 2) {
      errorReporter.atNode(
        node.returnType,
        CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_2,
        arguments: names,
      );
    } else {
      errorReporter.atNode(
        node.returnType,
        CompileTimeErrorCode.FINAL_NOT_INITIALIZED_CONSTRUCTOR_3_PLUS,
        arguments: [names[0], names[1], names.length - 2],
      );
    }
  }

  void reportNotInitializedNonNullable(
    List<_Field> notInitNonNullableFields,
  ) {
    if (notInitNonNullableFields.isEmpty) {
      return;
    }

    var names = notInitNonNullableFields.map((f) => f.name).toList();
    names.sort();

    for (var name in names) {
      errorReporter.atNode(
        node.returnType,
        CompileTimeErrorCode
            .NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD_CONSTRUCTOR,
        arguments: [name],
      );
    }
  }

  void updateWithInitializers(
    ErrorReporter errorReporter,
    ConstructorDeclaration node,
  ) {
    for (var initializer in node.initializers) {
      if (initializer is RedirectingConstructorInvocation) {
        hasRedirectingConstructorInvocation = true;
      }
      if (initializer is ConstructorFieldInitializer) {
        var fieldName = initializer.fieldName;
        var fieldElement = fieldName.element;
        if (fieldElement is FieldElement2) {
          var state = fields[fieldElement];
          if (state == _InitState.notInit) {
            fields[fieldElement] = _InitState.initInInitializer;
          } else if (state == _InitState.initInDeclaration) {
            if (fieldElement.isFinal || fieldElement.isConst) {
              errorReporter.atNode(
                fieldName,
                CompileTimeErrorCode
                    .FIELD_INITIALIZED_IN_INITIALIZER_AND_DECLARATION,
              );
            }
          } else if (state == _InitState.initInFieldFormal) {
            errorReporter.atNode(
              fieldName,
              CompileTimeErrorCode
                  .FIELD_INITIALIZED_IN_PARAMETER_AND_INITIALIZER,
            );
          } else if (state == _InitState.initInInitializer) {
            errorReporter.atNode(
              fieldName,
              CompileTimeErrorCode.FIELD_INITIALIZED_BY_MULTIPLE_INITIALIZERS,
              arguments: [fieldElement.displayName],
            );
          }
        }
      }
    }
  }

  void updateWithParameters(
    ConstructorDeclaration node,
  ) {
    var formalParameters = node.parameters.parameters;
    for (var parameter in formalParameters) {
      parameter = parameter.notDefault;
      if (parameter is FieldFormalParameterImpl) {
        var parameterFragment = parameter.declaredFragment!;
        var fieldElement = parameterFragment.element.field2;
        if (fieldElement == null) {
          continue;
        }
        _InitState? state = fields[fieldElement];
        if (state == _InitState.notInit) {
          fields[fieldElement] = _InitState.initInFieldFormal;
        } else if (state == _InitState.initInDeclaration) {
          if (fieldElement.isFinal || fieldElement.isConst) {
            errorReporter.atToken(
              parameter.name,
              CompileTimeErrorCode
                  .FINAL_INITIALIZED_IN_DECLARATION_AND_CONSTRUCTOR,
              arguments: [fieldElement.displayName],
            );
          }
        } else if (state == _InitState.initInFieldFormal) {
          // Reported in DuplicateDefinitionVerifier._checkDuplicateIdentifier
        }
      }
    }
  }
}

/// The field with a non `null` name.
class _Field {
  final FieldElement2 element;
  final String name;

  _Field(this.element, this.name);
}

/// The four states of a field initialization state through a constructor
/// signature, not initialized, initialized in the field declaration,
/// initialized in the field formal, and finally, initialized in the
/// initializers list.
enum _InitState {
  /// The field is declared without an initializer.
  notInit,

  /// The field is declared with an initializer.
  initInDeclaration,

  /// The field is initialized in a field formal parameter of the constructor
  /// being verified.
  initInFieldFormal,

  /// The field is initialized in the list of initializers of the constructor
  /// being verified.
  initInInitializer,
}

class _Interface {
  final TypeSystemImpl typeSystem;
  final InterfaceElement2 element;

  /// [_InitState.notInit] or [_InitState.initInDeclaration] for each field
  /// in [element]. This map works as the initial state for
  /// [_Constructor].
  final Map<FieldElement2, _InitState> fields;

  final Map<ConstructorElement2, _Constructor> constructors = Map.identity();

  _Interface({
    required this.typeSystem,
    required this.element,
    required this.fields,
  });

  _Constructor forConstructor({
    required ErrorReporter errorReporter,
    required ConstructorDeclaration node,
    required ConstructorFragment fragment,
  }) {
    var element = fragment.element;
    return constructors[element] ??= _Constructor(
      typeSystem: typeSystem,
      errorReporter: errorReporter,
      node: node,
      element: element,
      fields: {...fields},
    );
  }
}
