// 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/ast/syntactic_entity.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:collection/collection.dart';

// TODO(scheglov): https://github.com/dart-lang/sdk/issues/43608
Element2? _readElement2(AstNode node) {
  var parent = node.parent;

  if (parent is AssignmentExpression && parent.leftHandSide == node) {
    return parent.readElement2;
  }
  if (parent is PostfixExpression && parent.operand == node) {
    return parent.readElement2;
  }
  if (parent is PrefixExpression && parent.operand == node) {
    return parent.readElement2;
  }

  if (parent is PrefixedIdentifier && parent.identifier == node) {
    return _readElement2(parent);
  }
  if (parent is PropertyAccess && parent.propertyName == node) {
    return _readElement2(parent);
  }
  return null;
}

// TODO(scheglov): https://github.com/dart-lang/sdk/issues/43608
Element2? _writeElement2(AstNode node) {
  var parent = node.parent;

  if (parent is AssignmentExpression && parent.leftHandSide == node) {
    return parent.writeElement2;
  }
  if (parent is PostfixExpression && parent.operand == node) {
    return parent.writeElement2;
  }
  if (parent is PrefixExpression && parent.operand == node) {
    return parent.writeElement2;
  }

  if (parent is PrefixedIdentifier && parent.identifier == node) {
    return _writeElement2(parent);
  }
  if (parent is PropertyAccess && parent.propertyName == node) {
    return _writeElement2(parent);
  }
  return null;
}

// TODO(scheglov): https://github.com/dart-lang/sdk/issues/43608
DartType? _writeType(AstNode node) {
  var parent = node.parent;

  if (parent is AssignmentExpression && parent.leftHandSide == node) {
    return parent.writeType;
  }
  if (parent is PostfixExpression && parent.operand == node) {
    return parent.writeType;
  }
  if (parent is PrefixExpression && parent.operand == node) {
    return parent.writeType;
  }

  if (parent is PrefixedIdentifier && parent.identifier == node) {
    return _writeType(parent);
  }
  if (parent is PropertyAccess && parent.propertyName == node) {
    return _writeType(parent);
  }
  return null;
}

extension ArgumentListExtension on ArgumentList {
  /// Returns the named expression with the given [name], or `null` if none.
  NamedExpression? byName(String name) => arguments
      .whereType<NamedExpression>()
      .firstWhereOrNull((e) => e.name.label.name == name);

  /// Returns the argument with the given [index], or `null` if none.
  Expression? elementAtOrNull(int index) {
    if (index < arguments.length) {
      return arguments[index];
    }
    return null;
  }
}

extension ConstructorDeclarationExtension on ConstructorDeclaration {
  bool get isNonRedirectingGenerative {
    // Must be generative.
    if (externalKeyword != null || factoryKeyword != null) {
      return false;
    }

    // Must be non-redirecting.
    for (var initializer in initializers) {
      if (initializer is RedirectingConstructorInvocation) {
        return false;
      }
    }

    return true;
  }
}

extension DartPatternExtension on DartPattern {
  /// Return the matched value type of this pattern.
  ///
  /// This accessor should be used on patterns that are expected to
  /// be already resolved. Every such pattern must have the type set.
  DartType get matchedValueTypeOrThrow {
    var type = matchedValueType;
    if (type == null) {
      throw StateError('No type: $this');
    }
    return type;
  }

  DartType? get requiredType {
    var self = this;
    if (self is DeclaredVariablePattern) {
      return self.type?.typeOrThrow;
    } else if (self is ListPattern) {
      return self.requiredType;
    } else if (self is MapPattern) {
      return self.requiredType;
    } else if (self is WildcardPattern) {
      return self.type?.typeOrThrow;
    }
    return null;
  }
}

extension ExpressionExtension on Expression {
  /// Return the static type of this expression.
  ///
  /// This accessor should be used on expressions that are expected to
  /// be already resolved. Every such expression must have the type set,
  /// at least `dynamic`.
  TypeImpl get typeOrThrow => (this as ExpressionImpl).typeOrThrow;
}

extension ExpressionImplExtension on ExpressionImpl {
  /// Return the static type of this expression.
  ///
  /// This accessor should be used on expressions that are expected to
  /// be already resolved. Every such expression must have the type set,
  /// at least `dynamic`.
  TypeImpl get typeOrThrow {
    var type = staticType;
    if (type == null) {
      throw StateError('No type: $this');
    }
    return type;
  }
}

extension FormalParameterExtension on FormalParameter {
  bool get isOfLocalFunction {
    return thisOrAncestorOfType<FunctionBody>() != null;
  }

  FormalParameter get notDefault {
    var self = this;
    if (self is DefaultFormalParameter) {
      return self.parameter;
    }
    return self;
  }

  FormalParameterList get parentFormalParameterList {
    var parent = this.parent;
    if (parent is DefaultFormalParameter) {
      parent = parent.parent;
    }
    return parent as FormalParameterList;
  }

  AstNode get typeOrSelf {
    var self = this;
    if (self is SimpleFormalParameter) {
      var type = self.type;
      if (type != null) {
        return type;
      }
    }
    return self;
  }
}

// TODO(scheglov): https://github.com/dart-lang/sdk/issues/43608
extension IdentifierExtension on Identifier {
  Element2? get readElement2 {
    return _readElement2(this);
  }

  SimpleIdentifier get simpleName {
    var self = this;
    if (self is SimpleIdentifier) {
      return self;
    } else {
      return (self as PrefixedIdentifier).identifier;
    }
  }

  Element2? get writeElement2 {
    return _writeElement2(this);
  }

  Element2? get writeOrReadElement2 {
    return _writeElement2(this) ?? element;
  }

  DartType? get writeOrReadType {
    return _writeType(this) ?? staticType;
  }
}

extension IdentifierImplExtension on IdentifierImpl {
  NamedTypeImpl toNamedType({
    required TypeArgumentListImpl? typeArguments,
    required Token? question,
  }) {
    var self = this;
    if (self is PrefixedIdentifierImpl) {
      return NamedTypeImpl(
        importPrefix: ImportPrefixReferenceImpl(
          name: self.prefix.token,
          period: self.period,
        )..element2 = self.prefix.element,
        name2: self.identifier.token,
        typeArguments: typeArguments,
        question: question,
      )..element2 = self.identifier.element;
    } else if (self is SimpleIdentifierImpl) {
      return NamedTypeImpl(
        importPrefix: null,
        name2: self.token,
        typeArguments: typeArguments,
        question: question,
      )..element2 = self.element;
    } else {
      throw UnimplementedError('(${self.runtimeType}) $self');
    }
  }
}

// TODO(scheglov): https://github.com/dart-lang/sdk/issues/43608
extension IndexExpressionExtension on IndexExpression {
  Element2? get writeOrReadElement2 {
    return _writeElement2(this) ?? element;
  }
}

extension ListOfFormalParameterExtension on List<FormalParameter> {
  Iterable<FormalParameterImpl> get asImpl {
    return cast<FormalParameterImpl>();
  }
}

extension MethodDeclarationExtension on MethodDeclaration {
  bool get hasObjectMemberName {
    return const {
      '==',
      'hashCode',
      'toString',
      'runtimeType',
      'noSuchMethod',
    }.contains(name.lexeme);
  }
}

extension NamedTypeExtension on NamedType {
  String get qualifiedName {
    var importPrefix = this.importPrefix;
    if (importPrefix != null) {
      return '${importPrefix.name.lexeme}.${name2.lexeme}';
    } else {
      return name2.lexeme;
    }
  }
}

extension NullableStringExtension on String? {
  bool get isEmptyOrNull {
    var str = this;
    if (str == null) return true;
    if (str.isEmpty) return true;
    return false;
  }
}

extension PatternFieldImplExtension on PatternFieldImpl {
  /// A [SyntacticEntity] which can be used in error reporting, which is valid
  /// for both explicit getter names (like `Rect(width: var w, height: var h)`)
  /// and implicit getter names (like `Rect(:var width, :var height)`).
  SyntacticEntity get errorEntity {
    var fieldName = name;
    if (fieldName == null) {
      return this;
    }
    var fieldNameName = fieldName.name;
    if (fieldNameName == null) {
      var variablePattern = pattern.variablePattern;
      return variablePattern?.name ?? this;
    } else {
      return fieldNameName;
    }
  }
}

extension RecordTypeAnnotationExtension on RecordTypeAnnotation {
  List<RecordTypeAnnotationField> get fields {
    return [
      ...positionalFields,
      ...?namedFields?.fields,
    ];
  }
}

extension TypeAnnotationExtension on TypeAnnotation {
  /// Return the static type of this type annotation.
  ///
  /// This accessor should be used on expressions that are expected to
  /// be already resolved. Every such expression must have the type set,
  /// at least `dynamic`.
  TypeImpl get typeOrThrow => (this as TypeAnnotationImpl).typeOrThrow;
}

extension TypeAnnotationImplExtension on TypeAnnotationImpl {
  /// Return the static type of this type annotation.
  ///
  /// This accessor should be used on expressions that are expected to
  /// be already resolved. Every such expression must have the type set,
  /// at least `dynamic`.
  TypeImpl get typeOrThrow {
    var type = this.type;
    if (type == null) {
      throw StateError('No type: $this');
    }
    return type;
  }
}
