// 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/element.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';

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

  void changeVariance() {}

  DartType? createFunctionType({
    required FunctionType type,
    required InstantiatedTypeAliasElement? newAlias,
    required List<TypeParameterElement>? newTypeParameters,
    required List<ParameterElement>? newParameters,
    required DartType? newReturnType,
    required NullabilitySuffix? newNullability,
  }) {
    if (newAlias == null &&
        newNullability == null &&
        newReturnType == null &&
        newParameters == null) {
      return null;
    }

    return FunctionTypeImpl(
      typeFormals: newTypeParameters ?? type.typeFormals,
      parameters: newParameters ?? type.parameters,
      returnType: newReturnType ?? type.returnType,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      alias: newAlias ?? type.alias,
    );
  }

  DartType? createFunctionTypeBuilder({
    required FunctionTypeBuilder type,
    required List<TypeParameterElement>? newTypeParameters,
    required List<ParameterElement>? newParameters,
    required DartType? newReturnType,
    required NullabilitySuffix? newNullability,
  }) {
    if (newNullability == null &&
        newReturnType == null &&
        newParameters == null) {
      return null;
    }

    return FunctionTypeBuilder(
      newTypeParameters ?? type.typeFormals,
      newParameters ?? type.parameters,
      newReturnType ?? type.returnType,
      newNullability ?? type.nullabilitySuffix,
    );
  }

  DartType? createInterfaceType({
    required InterfaceType type,
    required InstantiatedTypeAliasElement? newAlias,
    required List<DartType>? newTypeArguments,
    required NullabilitySuffix? newNullability,
  }) {
    if (newAlias == null &&
        newTypeArguments == null &&
        newNullability == null) {
      return null;
    }

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

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

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

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

    return (type as TypeImpl).withNullability(newNullability);
  }

  DartType? 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(
      element: type.element,
      nullabilitySuffix: newNullability ?? type.nullabilitySuffix,
      promotedBound: newPromotedBound ?? promotedBound,
      alias: type.alias,
    );
  }

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

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

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

  @override
  DartType? visitFunctionType(FunctionType node) {
    var newNullability = visitNullability(node);

    List<TypeParameterElement>? newTypeParameters;
    for (var i = 0; i < node.typeFormals.length; i++) {
      var typeParameter = node.typeFormals[i];
      var bound = typeParameter.bound;
      if (bound != null) {
        var newBound = visitTypeParameterBound(bound);
        if (newBound != null) {
          newTypeParameters ??= node.typeFormals.toList(growable: false);
          newTypeParameters[i] = TypeParameterElementImpl.synthetic(
            typeParameter.name,
          )..bound = newBound;
        }
      }
    }

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

      substitution = Substitution.fromMap(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 as TypeParameterElementImpl).bound = newBound;
        }
      }
    }

    DartType? 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);

    InstantiatedTypeAliasElement? newAlias;
    var alias = node.alias;
    if (alias != null) {
      List<DartType>? 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(
          element: alias.element,
          typeArguments: newArguments,
        );
      }
    }

    changeVariance();

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

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

      // ignore: deprecated_member_use_from_same_package
      var kind = parameter.parameterKind;
      var newKind = visitParameterKind(kind);

      if (newType != null || newKind != null) {
        newParameters ??= node.parameters.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
  DartType? visitFunctionTypeBuilder(FunctionTypeBuilder node) {
    var newNullability = visitNullability(node);

    List<TypeParameterElement>? newTypeParameters;
    for (var i = 0; i < node.typeFormals.length; i++) {
      var typeParameter = node.typeFormals[i];
      var bound = typeParameter.bound;
      if (bound != null) {
        var newBound = visitTypeParameterBound(bound);
        if (newBound != null) {
          newTypeParameters ??= node.typeFormals.toList(growable: false);
          newTypeParameters[i] = TypeParameterElementImpl.synthetic(
            typeParameter.name,
          )..bound = newBound;
        }
      }
    }

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

      substitution = Substitution.fromMap(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 as TypeParameterElementImpl).bound = newBound;
        }
      }
    }

    DartType? 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<ParameterElement>? newParameters;
    for (var i = 0; i < node.parameters.length; i++) {
      var parameter = node.parameters[i];

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

      // ignore: deprecated_member_use_from_same_package
      var kind = parameter.parameterKind;
      var newKind = visitParameterKind(kind);

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

    changeVariance();

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

  @override
  DartType? visitInterfaceType(InterfaceType type) {
    var newNullability = visitNullability(type);

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

    var newTypeArguments = _typeArguments(
      type.element.typeParameters,
      type.typeArguments,
    );

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

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

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

    var parameters = const <TypeParameterElement>[];
    var element = type.element;
    if (element is InterfaceElement) {
      parameters = element.typeParameters;
    } else if (element is TypeAliasElement) {
      parameters = element.typeParameters;
    }

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

  @override
  DartType? visitNeverType(NeverType type) {
    var newNullability = visitNullability(type);

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

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

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

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

    InstantiatedTypeAliasElement? newAlias;
    var alias = type.alias;
    if (alias != null) {
      var newArguments = _typeArguments(
        alias.element.typeParameters,
        alias.typeArguments,
      );
      if (newArguments != null) {
        newAlias = InstantiatedTypeAliasElementImpl(
          element: alias.element,
          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
  DartType? 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,
    );
  }

  DartType? visitTypeArgument(
    TypeParameterElement parameter,
    DartType argument,
  ) {
    return argument.accept(this);
  }

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

  @override
  DartType? visitTypeParameterType(TypeParameterType type) {
    var newNullability = visitNullability(type);

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

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

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

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

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

    List<DartType>? 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;
  }
}
