// Copyright (c) 2020, 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/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_visitor.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_algebra.dart';
import 'package:analyzer/src/dart/element/type_schema.dart';
import 'package:analyzer/src/dart/element/type_visitor.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/summary2/function_type_builder.dart';
import 'package:analyzer/src/summary2/named_type_builder.dart';
import 'package:analyzer/src/summary2/record_type_builder.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';

/// Helper visitor that clones a type if a nested type is replaced, and
/// otherwise returns `null`.
class ReplacementVisitor
    implements
        TypeVisitor<TypeImpl?>,
        InferenceTypeVisitor<TypeImpl?>,
        LinkingTypeVisitor<TypeImpl?> {
  const ReplacementVisitor();

  void changeVariance() {}

  FunctionTypeImpl? createFunctionType({
    required FunctionTypeImpl type,
    required InstantiatedTypeAliasElementImpl? newAlias,
    required List<TypeParameterElementImpl2>? newTypeParameters,
    required List<FormalParameterElementMixin>? newParameters,
    required TypeImpl? newReturnType,
    required NullabilitySuffix? newNullability,
  }) {
    if (newAlias == null &&
        newNullability == null &&
        newReturnType == null &&
        newParameters == null) {
      return null;
    }

    return FunctionTypeImpl.v2(
      typeParameters: newTypeParameters ?? type.typeParameters,
      formalParameters: newParameters ?? type.formalParameters,
      returnType: newReturnType ?? type.returnType,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      alias: newAlias ?? type.alias,
    );
  }

  FunctionTypeBuilder? createFunctionTypeBuilder({
    required FunctionTypeBuilder type,
    required List<TypeParameterElementImpl2>? newTypeParameters,
    required List<FormalParameterElementImpl>? newFormalParameters,
    required TypeImpl? newReturnType,
    required NullabilitySuffix? newNullability,
  }) {
    if (newNullability == null &&
        newReturnType == null &&
        newFormalParameters == null) {
      return null;
    }

    return FunctionTypeBuilder.v2(
      typeParameters: newTypeParameters ?? type.typeParameters,
      formalParameters: newFormalParameters ?? type.formalParameters,
      returnType: newReturnType ?? type.returnType,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
    );
  }

  InterfaceTypeImpl? createInterfaceType({
    required InterfaceTypeImpl type,
    required InstantiatedTypeAliasElementImpl? newAlias,
    required List<TypeImpl>? newTypeArguments,
    required NullabilitySuffix? newNullability,
  }) {
    if (newAlias == null &&
        newTypeArguments == null &&
        newNullability == null) {
      return null;
    }

    return InterfaceTypeImpl(
      element: type.element3,
      typeArguments: newTypeArguments ?? type.typeArguments,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      alias: newAlias ?? type.alias,
    );
  }

  NamedTypeBuilder? createNamedTypeBuilder({
    required NamedTypeBuilder type,
    required List<TypeImpl>? newTypeArguments,
    required NullabilitySuffix? newNullability,
  }) {
    if (newTypeArguments == null && newNullability == null) {
      return null;
    }

    return NamedTypeBuilder.v2(
      linker: type.linker,
      typeSystem: type.typeSystem,
      element: type.element3,
      arguments: newTypeArguments ?? type.arguments,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
    );
  }

  NeverTypeImpl? createNeverType({
    required NeverTypeImpl type,
    required NullabilitySuffix? newNullability,
  }) {
    if (newNullability == null) {
      return null;
    }

    return type.withNullability(newNullability);
  }

  TypeParameterTypeImpl? createPromotedTypeParameterType({
    required TypeParameterType type,
    required NullabilitySuffix? newNullability,
    required DartType? newPromotedBound,
  }) {
    if (newNullability == null && newPromotedBound == null) {
      return null;
    }

    var promotedBound = (type as TypeParameterTypeImpl).promotedBound;
    return TypeParameterTypeImpl(
      element3: type.element3,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      promotedBound: newPromotedBound ?? promotedBound,
      alias: type.alias,
    );
  }

  TypeParameterTypeImpl? createTypeParameterType({
    required TypeParameterTypeImpl type,
    required NullabilitySuffix? newNullability,
  }) {
    if (newNullability == null) {
      return null;
    }

    return TypeParameterTypeImpl(
      element3: type.element3,
      nullabilitySuffix: newNullability,
      alias: type.alias,
    );
  }

  @override
  TypeImpl? visitDynamicType(DynamicType type) {
    return null;
  }

  @override
  TypeImpl? visitFunctionType(FunctionType node) {
    // TODO(scheglov): avoid this cast
    node as FunctionTypeImpl;
    var newNullability = visitNullability(node);

    List<TypeParameterElementImpl2>? newTypeParameters;
    for (var i = 0; i < node.typeParameters.length; i++) {
      var typeParameter = node.typeParameters[i];
      var bound = typeParameter.bound;
      if (bound != null) {
        var newBound = visitTypeParameterBound(bound);
        if (newBound != null) {
          newTypeParameters ??= node.typeParameters.toList(growable: false);
          newTypeParameters[i] = typeParameter.freshCopy()
            ..bound =
                // TODO(paulberry): eliminate this cast by changing the return
                // type of `visitTypeParameterBound`.
                newBound as TypeImpl;
        }
      }
    }

    Substitution? substitution;
    if (newTypeParameters != null) {
      var map = <TypeParameterElement2, DartType>{};
      for (var i = 0; i < newTypeParameters.length; ++i) {
        var typeParameter = node.typeParameters[i];
        var newTypeParameter = newTypeParameters[i];
        map[typeParameter] = newTypeParameter.instantiate(
          nullabilitySuffix: NullabilitySuffix.none,
        );
      }

      substitution = Substitution.fromMap2(map);

      for (var i = 0; i < newTypeParameters.length; i++) {
        var newTypeParameter = newTypeParameters[i];
        var bound = newTypeParameter.bound;
        if (bound != null) {
          var newBound = substitution.substituteType(bound);
          newTypeParameter.bound = newBound;
        }
      }
    }

    TypeImpl? visitType(TypeImpl? type) {
      if (type == null) return null;
      var result = type.accept(this);
      if (substitution != null) {
        result = substitution.substituteType(result ?? type);
      }
      return result;
    }

    var newReturnType = visitType(node.returnType);

    InstantiatedTypeAliasElementImpl? newAlias;
    var alias = node.alias;
    if (alias != null) {
      List<TypeImpl>? newArguments;
      var aliasArguments = alias.typeArguments;
      for (var i = 0; i < aliasArguments.length; i++) {
        var substitution = aliasArguments[i].accept(this);
        if (substitution != null) {
          newArguments ??= aliasArguments.toList(growable: false);
          newArguments[i] = substitution;
        }
      }
      if (newArguments != null) {
        newAlias = InstantiatedTypeAliasElementImpl.v2(
          element: alias.element2,
          typeArguments: newArguments,
        );
      }
    }

    changeVariance();

    List<FormalParameterElementMixin>? newParameters;
    for (var i = 0; i < node.formalParameters.length; i++) {
      var parameter = node.formalParameters[i];

      var type = parameter.type;
      var newType = visitType(type);

      var kind = parameter.parameterKind;
      var newKind = visitParameterKind(kind);

      if (newType != null || newKind != null) {
        newParameters ??= node.formalParameters.toList(growable: false);
        newParameters[i] = parameter.copyWith(
          type: newType,
          kind: newKind,
        );
      }
    }

    changeVariance();

    return createFunctionType(
      type: node,
      newAlias: newAlias,
      newTypeParameters: newTypeParameters,
      newParameters: newParameters,
      newReturnType: newReturnType,
      newNullability: newNullability,
    );
  }

  @override
  TypeImpl? visitFunctionTypeBuilder(FunctionTypeBuilder node) {
    var newNullability = visitNullability(node);

    List<TypeParameterElementImpl2>? newTypeParameters;
    for (var i = 0; i < node.typeParameters.length; i++) {
      var typeParameter = node.typeParameters[i];
      var bound = typeParameter.bound;
      if (bound != null) {
        var newBound = visitTypeParameterBound(bound);
        if (newBound != null) {
          newTypeParameters ??= node.typeParameters.toList(growable: false);
          newTypeParameters[i] = typeParameter.freshCopy()
            ..bound =
                // TODO(paulberry): eliminate this cast by changing the return
                // type of `visitTypeParameterBound`.
                newBound as TypeImpl;
        }
      }
    }

    Substitution? substitution;
    if (newTypeParameters != null) {
      var map = <TypeParameterElement2, DartType>{};
      for (var i = 0; i < newTypeParameters.length; ++i) {
        var typeParameter = node.typeParameters[i];
        var newTypeParameter = newTypeParameters[i];
        map[typeParameter] = newTypeParameter.instantiate(
          nullabilitySuffix: NullabilitySuffix.none,
        );
      }

      substitution = Substitution.fromMap2(map);

      for (var i = 0; i < newTypeParameters.length; i++) {
        var newTypeParameter = newTypeParameters[i];
        var bound = newTypeParameter.bound;
        if (bound != null) {
          var newBound = substitution.substituteType(bound);
          newTypeParameter.bound = newBound;
        }
      }
    }

    TypeImpl? visitType(DartType? type) {
      if (type == null) return null;
      var result = type.accept(this);
      if (substitution != null) {
        result = substitution.substituteType(result ?? type);
      }
      return result;
    }

    var newReturnType = visitType(node.returnType);

    changeVariance();

    List<FormalParameterElementImpl>? newFormalParameters;
    for (var i = 0; i < node.formalParameters.length; i++) {
      var parameter = node.formalParameters[i];

      var type = parameter.type;
      var newType = visitType(type);

      var kind = parameter.parameterKind;
      var newKind = visitParameterKind(kind);

      if (newType != null || newKind != null) {
        newFormalParameters ??= node.formalParameters.toList(growable: false);
        newFormalParameters[i] = parameter.copyWith(
          type: newType,
          kind: newKind,
        );
      }
    }

    changeVariance();

    return createFunctionTypeBuilder(
      type: node,
      newTypeParameters: newTypeParameters,
      newFormalParameters: newFormalParameters,
      newReturnType: newReturnType,
      newNullability: newNullability,
    );
  }

  @override
  TypeImpl? visitInterfaceType(covariant InterfaceTypeImpl type) {
    var newNullability = visitNullability(type);

    InstantiatedTypeAliasElementImpl? newAlias;
    var alias = type.alias;
    if (alias != null) {
      var newArguments = _typeArguments(
        alias.element2.typeParameters2,
        alias.typeArguments,
      );
      if (newArguments != null) {
        newAlias = InstantiatedTypeAliasElementImpl.v2(
          element: alias.element2,
          typeArguments: newArguments,
        );
      }
    }

    var newTypeArguments = _typeArguments(
      type.element3.typeParameters2,
      type.typeArguments,
    );

    return createInterfaceType(
      type: type,
      newAlias: newAlias,
      newTypeArguments: newTypeArguments,
      newNullability: newNullability,
    );
  }

  @override
  TypeImpl? visitInvalidType(InvalidType type) {
    return null;
  }

  @override
  TypeImpl? visitNamedTypeBuilder(NamedTypeBuilder type) {
    var newNullability = visitNullability(type);

    var parameters = const <TypeParameterElementImpl2>[];
    var element = type.element3;
    if (element is InterfaceElementImpl2) {
      parameters = element.typeParameters2;
    } else if (element is TypeAliasElementImpl2) {
      parameters = element.typeParameters2;
    }

    var newArguments = _typeArguments(parameters, type.arguments);
    return createNamedTypeBuilder(
      type: type,
      newTypeArguments: newArguments,
      newNullability: newNullability,
    );
  }

  @override
  TypeImpl? visitNeverType(covariant NeverTypeImpl type) {
    var newNullability = visitNullability(type);

    return createNeverType(
      type: type,
      newNullability: newNullability,
    );
  }

  NullabilitySuffix? visitNullability(DartType type) {
    return null;
  }

  ParameterKind? visitParameterKind(ParameterKind kind) {
    return null;
  }

  @override
  TypeImpl? visitRecordType(covariant RecordTypeImpl type) {
    var newNullability = visitNullability(type);

    InstantiatedTypeAliasElementImpl? newAlias;
    var alias = type.alias;
    if (alias != null) {
      var newArguments = _typeArguments(
        alias.element2.typeParameters2,
        alias.typeArguments,
      );
      if (newArguments != null) {
        newAlias = InstantiatedTypeAliasElementImpl.v2(
          element: alias.element2,
          typeArguments: newArguments,
        );
      }
    }

    List<RecordTypePositionalFieldImpl>? newPositionalFields;
    var positionalFields = type.positionalFields;
    for (var i = 0; i < positionalFields.length; i++) {
      var field = positionalFields[i];
      var newType = field.type.accept(this);
      if (newType != null) {
        newPositionalFields ??= positionalFields.toList(growable: false);
        newPositionalFields[i] = RecordTypePositionalFieldImpl(
          type: newType,
        );
      }
    }

    List<RecordTypeNamedFieldImpl>? newNamedFields;
    var namedFields = type.namedFields;
    for (var i = 0; i < namedFields.length; i++) {
      var field = namedFields[i];
      var newType = field.type.accept(this);
      if (newType != null) {
        newNamedFields ??= namedFields.toList(growable: false);
        newNamedFields[i] = RecordTypeNamedFieldImpl(
          name: field.name,
          type: newType,
        );
      }
    }

    if (newAlias == null &&
        newPositionalFields == null &&
        newNamedFields == null &&
        newNullability == null) {
      return null;
    }

    return RecordTypeImpl(
      positionalFields: newPositionalFields ?? type.positionalFields,
      namedFields: newNamedFields ?? type.namedFields,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      alias: newAlias ?? type.alias,
    );
  }

  @override
  TypeImpl? visitRecordTypeBuilder(RecordTypeBuilder type) {
    List<DartType>? newFieldTypes;
    var fieldTypes = type.fieldTypes;
    for (var i = 0; i < fieldTypes.length; i++) {
      var fieldType = fieldTypes[i];
      var newFieldType = fieldType.accept(this);
      if (newFieldType != null) {
        newFieldTypes ??= fieldTypes.toList(growable: false);
        newFieldTypes[i] = newFieldType;
      }
    }

    var newNullability = visitNullability(type);

    if (newFieldTypes == null && newNullability == null) {
      return null;
    }

    return RecordTypeBuilder(
      typeSystem: type.typeSystem,
      node: type.node,
      fieldTypes: newFieldTypes ?? type.fieldTypes,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
    );
  }

  TypeImpl? visitTypeArgument(
    TypeParameterElementImpl2 parameter,
    TypeImpl argument,
  ) {
    return argument.accept(this);
  }

  DartType? visitTypeParameterBound(DartType type) {
    return type.accept(this);
  }

  @override
  TypeImpl? visitTypeParameterType(TypeParameterType type) {
    // TODO(scheglov): avoid this cast
    type as TypeParameterTypeImpl;
    var newNullability = visitNullability(type);

    var promotedBound = type.promotedBound;
    if (promotedBound != null) {
      var newPromotedBound = promotedBound.accept(this);
      return createPromotedTypeParameterType(
        type: type,
        newNullability: newNullability,
        newPromotedBound: newPromotedBound,
      );
    }

    return createTypeParameterType(
      type: type,
      newNullability: newNullability,
    );
  }

  @override
  TypeImpl? visitUnknownInferredType(UnknownInferredType type) {
    return null;
  }

  @override
  TypeImpl? visitVoidType(VoidType type) {
    return null;
  }

  List<TypeImpl>? _typeArguments(
    List<TypeParameterElementImpl2> parameters,
    List<TypeImpl> arguments,
  ) {
    if (arguments.length != parameters.length) {
      return null;
    }

    List<TypeImpl>? newArguments;
    for (var i = 0; i < arguments.length; i++) {
      var substitution = visitTypeArgument(parameters[i], arguments[i]);
      if (substitution != null) {
        newArguments ??= arguments.toList(growable: false);
        newArguments[i] = substitution;
      }
    }

    return newArguments;
  }
}
