// Copyright (c) 2017, 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/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:linter/src/analyzer.dart';
import 'package:linter/src/util/dart_type_utilities.dart';

const _desc = r'Use initializing formals when possible.';

const _details = r'''

**DO** use initializing formals when possible.

Using initializing formals when possible makes your code more terse.

**BAD:**
```
class Point {
  num x, y;
  Point(num x, num y) {
    this.x = x;
    this.y = y;
  }
}
```

**GOOD:**
```
class Point {
  num x, y;
  Point(this.x, this.y);
}
```

''';

Iterable<AssignmentExpression> _getAssignmentExpressionsInConstructorBody(
    ConstructorDeclaration node) {
  final body = node.body;
  final statements =
      (body is BlockFunctionBody) ? body.block.statements : <Statement>[];
  return statements
      .where((e) => e is ExpressionStatement)
      .map((e) => (e as ExpressionStatement).expression)
      .where((e) => e is AssignmentExpression)
      .map((e) => e as AssignmentExpression);
}

Iterable<ConstructorFieldInitializer>
    _getConstructorFieldInitializersInInitializers(
            ConstructorDeclaration node) =>
        node.initializers
            .where((e) => e is ConstructorFieldInitializer)
            .map((e) => (e as ConstructorFieldInitializer));

Element _getLeftElement(AssignmentExpression assignment) =>
    DartTypeUtilities.getCanonicalElementFromIdentifier(
        assignment.leftHandSide);

Iterable<Element> _getParameters(ConstructorDeclaration node) =>
    node.parameters.parameters.map((e) => e.identifier.staticElement);

Element _getRightElement(AssignmentExpression assignment) =>
    DartTypeUtilities.getCanonicalElementFromIdentifier(
        assignment.rightHandSide);

class PreferInitializingFormals extends LintRule
    implements NodeLintRuleWithContext {
  PreferInitializingFormals()
      : super(
            name: 'prefer_initializing_formals',
            description: _desc,
            details: _details,
            group: Group.style);

  @override
  void registerNodeProcessors(NodeLintRegistry registry,
      [LinterContext context]) {
    final visitor = new _Visitor(this);
    registry.addConstructorDeclaration(this, visitor);
  }
}

class _Visitor extends SimpleAstVisitor<void> {
  final LintRule rule;

  _Visitor(this.rule);

  @override
  void visitConstructorDeclaration(ConstructorDeclaration node) {
    final parameters = _getParameters(node);
    final parametersUsedOnce = new Set<Element>();
    final parametersUsedMoreThanOnce = new Set<Element>();

    bool isAssignmentExpressionToLint(AssignmentExpression assignment) {
      final leftElement = _getLeftElement(assignment);
      final rightElement = _getRightElement(assignment);
      return leftElement != null &&
          rightElement != null &&
          !leftElement.isPrivate &&
          leftElement is FieldElement &&
          !leftElement.isSynthetic &&
          leftElement.enclosingElement ==
              node.declaredElement.enclosingElement &&
          parameters.contains(rightElement) &&
          (!parametersUsedMoreThanOnce.contains(rightElement) ||
              leftElement.name == rightElement.name);
    }

    bool isConstructorFieldInitializerToLint(
        ConstructorFieldInitializer constructorFieldInitializer) {
      final expression = constructorFieldInitializer.expression;
      return !(constructorFieldInitializer.fieldName.staticElement?.isPrivate ??
              true) &&
          expression is SimpleIdentifier &&
          parameters.contains(expression.staticElement) &&
          (!parametersUsedMoreThanOnce.contains(expression.staticElement) ||
              constructorFieldInitializer.fieldName.staticElement?.name ==
                  expression.staticElement.name);
    }

    void processElement(Element element) {
      if (!parametersUsedOnce.add(element)) {
        parametersUsedMoreThanOnce.add(element);
      }
    }

    node.parameters.parameterElements
        .where((p) => p.isInitializingFormal)
        .forEach(processElement);

    _getAssignmentExpressionsInConstructorBody(node)
        .where(isAssignmentExpressionToLint)
        .map(_getRightElement)
        .forEach(processElement);

    _getConstructorFieldInitializersInInitializers(node)
        .where(isConstructorFieldInitializerToLint)
        .map((e) => (e.expression as SimpleIdentifier).staticElement)
        .forEach(processElement);

    _getAssignmentExpressionsInConstructorBody(node)
        .where(isAssignmentExpressionToLint)
        .forEach(rule.reportLint);

    _getConstructorFieldInitializersInInitializers(node)
        .where(isConstructorFieldInitializerToLint)
        .forEach(rule.reportLint);
  }
}
