// 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/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/type.dart';

/// Some [ConstructorElement]s can be temporary marked as "const" to check
/// if doing this is valid.
final temporaryConstConstructorElements = Expando<bool>();

/// Check if the [node] and all its sub-nodes are potentially constant.
///
/// Return the list of nodes that are not potentially constant.
List<AstNode> getNotPotentiallyConstants(
  AstNode node, {
  required FeatureSet featureSet,
}) {
  var collector = _Collector(featureSet: featureSet);
  collector.collect(node);
  return collector.nodes;
}

/// Return `true` if the [node] is a constant type expression.
bool isConstantTypeExpression(TypeAnnotation node) {
  return _ConstantTypeChecker(potentially: false).check(node);
}

/// Return `true` if the [node] is a potentially constant type expression.
bool isPotentiallyConstantTypeExpression(TypeAnnotation node) {
  return _ConstantTypeChecker(potentially: true).check(node);
}

bool _isConstantTypeName(Identifier name) {
  var element = name.element;
  if (element is InterfaceElement || element is TypeAliasElement) {
    if (name is PrefixedIdentifier) {
      if (name.isDeferred) {
        return false;
      }
    }
    return true;
  }
  return false;
}

class _Collector {
  final FeatureSet featureSet;
  final List<AstNode> nodes = [];

  _Collector({required this.featureSet});

  void collect(AstNode node) {
    if (node is BooleanLiteral ||
        node is DoubleLiteral ||
        node is IntegerLiteral ||
        node is NullLiteral ||
        node is SimpleStringLiteral ||
        node is SymbolLiteral) {
      return;
    }

    if (node is AdjacentStrings) {
      for (var string in node.strings) {
        collect(string);
      }
      return;
    }

    if (node is DotShorthandPropertyAccess) {
      return _identifier(node.propertyName);
    }

    if (node is DotShorthandConstructorInvocation) {
      if (!node.isConst) {
        nodes.add(node);
      }
      return;
    }

    if (node is StringInterpolation) {
      for (var component in node.elements) {
        if (component is InterpolationExpression) {
          collect(component.expression);
        }
      }
      return;
    }

    if (node is Identifier) {
      return _identifier(node);
    }

    if (node is InstanceCreationExpression) {
      if (!node.isConst) {
        nodes.add(node);
      }
      return;
    }

    if (node is TypedLiteral) {
      return _typedLiteral(node);
    }

    if (node is ParenthesizedExpression) {
      collect(node.expression);
      return;
    }

    if (node is RecordLiteral) {
      return _recordLiteral(node);
    }

    if (node is MethodInvocation) {
      return _methodInvocation(node);
    }

    if (node is NamedExpression) {
      return collect(node.expression);
    }

    if (node is BinaryExpression) {
      collect(node.leftOperand);
      collect(node.rightOperand);
      return;
    }

    if (node is PrefixExpression) {
      var operator = node.operator.type;
      if (operator == TokenType.BANG ||
          operator == TokenType.MINUS ||
          operator == TokenType.TILDE) {
        collect(node.operand);
        return;
      }
      nodes.add(node);
      return;
    }

    if (node is ConditionalExpression) {
      collect(node.condition);
      collect(node.thenExpression);
      collect(node.elseExpression);
      return;
    }

    if (node is PropertyAccess) {
      return _propertyAccess(node);
    }

    if (node is AsExpression) {
      if (featureSet.isEnabled(Feature.non_nullable)) {
        if (!isPotentiallyConstantTypeExpression(node.type)) {
          nodes.add(node.type);
        }
      } else {
        if (!isConstantTypeExpression(node.type)) {
          nodes.add(node.type);
        }
      }
      collect(node.expression);
      return;
    }

    if (node is IsExpression) {
      if (featureSet.isEnabled(Feature.non_nullable)) {
        if (!isPotentiallyConstantTypeExpression(node.type)) {
          nodes.add(node.type);
        }
      } else {
        if (!isConstantTypeExpression(node.type)) {
          nodes.add(node.type);
        }
      }
      collect(node.expression);
      return;
    }

    if (node is MapLiteralEntry) {
      collect(node.key);
      collect(node.value);
      return;
    }

    if (node is SpreadElement) {
      collect(node.expression);
      return;
    }

    if (node is IfElement) {
      collect(node.expression);
      collect(node.thenElement);
      if (node.elseElement != null) {
        collect(node.elseElement!);
      }
      return;
    }

    if (node is ConstructorReference) {
      _typeArgumentList(node.constructorName.type.typeArguments);
      return;
    }

    if (node is FunctionReference) {
      _typeArgumentList(node.typeArguments);
      collect(node.function);
      return;
    }

    if (node is TypeLiteral) {
      _typeArgumentList(node.type.typeArguments);
      return;
    }

    nodes.add(node);
  }

  void _identifier(Identifier node) {
    var element = node.element;

    if (node is PrefixedIdentifier) {
      if (node.isDeferred) {
        nodes.add(node);
        return;
      }
      if (node.identifier.name == 'length') {
        collect(node.prefix);
        return;
      }
      if (element is MethodElement && element.isStatic) {
        if (!_isConstantTypeName(node.prefix)) {
          nodes.add(node);
        }
        return;
      }
    }

    if (element is FormalParameterElement) {
      var enclosing = element.enclosingElement;
      if (enclosing is ConstructorElement &&
          isConstConstructorElement(enclosing)) {
        if (node.thisOrAncestorOfType<ConstructorInitializer>() != null) {
          return;
        }
      }
      nodes.add(node);
      return;
    }

    if (element is VariableElement) {
      if (!element.isConst) {
        nodes.add(node);
      }
      return;
    }
    if (element is GetterElement) {
      var variable = element.variable;
      if (!variable.isConst) {
        nodes.add(node);
      }
      return;
    }
    if (_isConstantTypeName(node)) {
      return;
    }
    if (element is TopLevelFunctionElement) {
      return;
    }
    if (element is MethodElement && element.isStatic) {
      return;
    }
    if (element is TypeParameterElement &&
        featureSet.isEnabled(Feature.constructor_tearoffs)) {
      return;
    }
    nodes.add(node);
  }

  void _methodInvocation(MethodInvocation node) {
    var arguments = node.argumentList.arguments;
    if (arguments.length == 2) {
      var element = node.methodName.element;
      if (element is TopLevelFunctionElement && element.isDartCoreIdentical) {
        collect(arguments[0]);
        collect(arguments[1]);
        return;
      }
    }
    // TODO(srawlins): collect type arguments.
    nodes.add(node);
  }

  void _propertyAccess(PropertyAccess node) {
    // CascadeExpression is not a constant, so the target is never null.
    var target = node.target!;

    if (node.propertyName.name == 'length') {
      collect(target);
      return;
    }

    if (target is PrefixedIdentifier) {
      if (target.isDeferred) {
        nodes.add(node);
        return;
      }

      var element = node.propertyName.element;
      if (element is GetterElement) {
        var variable = element.variable;
        if (!variable.isConst) {
          nodes.add(node.propertyName);
        }
        return;
      }
    }

    nodes.add(node);
  }

  void _recordLiteral(RecordLiteral node) {
    for (var field in node.fields) {
      collect(field);
    }
  }

  void _typeArgumentList(TypeArgumentList? typeArgumentList) {
    var typeArguments = typeArgumentList?.arguments;
    if (typeArguments != null) {
      for (var typeArgument in typeArguments) {
        if (!isPotentiallyConstantTypeExpression(typeArgument)) {
          nodes.add(typeArgument);
        }
      }
    }
  }

  void _typedLiteral(TypedLiteral node) {
    if (!node.isConst) {
      nodes.add(node);
      return;
    }

    if (node is ListLiteral) {
      var typeArguments = node.typeArguments?.arguments;
      if (typeArguments != null && typeArguments.length == 1) {
        var elementType = typeArguments[0];
        if (!isPotentiallyConstantTypeExpression(elementType)) {
          nodes.add(elementType);
        }
      }

      for (var element in node.elements) {
        collect(element);
      }
      return;
    }

    if (node is SetOrMapLiteral) {
      var typeArguments = node.typeArguments?.arguments;
      if (typeArguments != null && typeArguments.length == 1) {
        var elementType = typeArguments[0];
        if (!isPotentiallyConstantTypeExpression(elementType)) {
          nodes.add(elementType);
        }
      }

      if (typeArguments != null && typeArguments.length == 2) {
        var keyType = typeArguments[0];
        var valueType = typeArguments[1];
        if (!isConstantTypeExpression(keyType)) {
          nodes.add(keyType);
        }
        if (!isConstantTypeExpression(valueType)) {
          nodes.add(valueType);
        }
      }

      for (var element in node.elements) {
        collect(element);
      }
    }
  }

  static bool isConstConstructorElement(ConstructorElement element) {
    if (element.isConst) return true;
    return temporaryConstConstructorElements[element] ?? false;
  }
}

class _ConstantTypeChecker {
  final bool potentially;

  _ConstantTypeChecker({required this.potentially});

  /// Returns whether the [node] is a (potentially) constant type expression.
  bool check(TypeAnnotation? node) {
    if (node == null) {
      return false;
    }
    if (potentially &&
        node is NamedType &&
        node.element is TypeParameterElement) {
      return true;
    }

    return switch (node) {
      NamedType() => _checkNamedType(node),
      GenericFunctionType() => _checkGenericFunctionType(node),
      RecordTypeAnnotation() => _checkRecordTypeAnnotation(node),
    };
  }

  bool _checkGenericFunctionType(GenericFunctionType node) {
    var returnType = node.returnType;
    if (returnType != null) {
      if (!check(returnType)) {
        return false;
      }
    }

    var typeParameters = node.typeParameters?.typeParameters;
    if (typeParameters != null) {
      for (var parameter in typeParameters) {
        var bound = parameter.bound;
        if (bound != null && !check(bound)) {
          return false;
        }
      }
    }

    var formalParameters = node.parameters.parameters;
    for (var formalParameter in formalParameters) {
      if (formalParameter is SimpleFormalParameter) {
        if (!check(formalParameter.type)) {
          return false;
        }
      }
    }
    return true;
  }

  bool _checkNamedType(NamedType node) {
    if (node.isConstantNamedType) {
      var arguments = node.typeArguments?.arguments;
      if (arguments != null && arguments.any((argument) => !check(argument))) {
        return false;
      }
      return true;
    }
    var type = node.type;
    if (type is DynamicTypeImpl || type is NeverType || type is VoidType) {
      return true;
    }
    return false;
  }

  bool _checkRecordTypeAnnotation(RecordTypeAnnotation node) {
    if (node.fields.any((field) => !check(field.type))) {
      return false;
    }
    return true;
  }
}

extension on NamedType {
  bool get isConstantNamedType {
    var element = this.element;
    if (element is InterfaceElement || element is TypeAliasElement) {
      return !isDeferred;
    }
    return false;
  }
}
