// Copyright (c) 2016, 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/analyzer.dart';
import 'package:analyzer/dart/analysis/declared_variables.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/error/listener.dart';
import 'package:linter/src/analyzer.dart';
import 'package:linter/src/util/dart_type_utilities.dart';

const alwaysFalse =
    'Always false because indexOf is always greater or equal -1.';

const alwaysTrue = 'Always true because indexOf is always greater or equal -1.';

const useContains = 'Use contains instead of indexOf';
const _desc = r'Use contains for `List` and `String` instances.';

const _details = r'''

**DON'T** use `indexOf` to see if a collection contains an element.

Calling `indexOf` to see if a collection contains something is difficult to read
and may have poor performance.

Instead, prefer `contains`.

**GOOD:**
```
if (!lunchBox.contains('sandwich') return 'so hungry...';
```

**BAD:**
```
if (lunchBox.indexOf('sandwich') == -1 return 'so hungry...';
```

''';

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

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

  void reportLintWithDescription(AstNode node, String description) {
    if (node != null) {
      reporter.reportErrorForNode(new _LintCode(name, description), node, []);
    }
  }
}

/// TODO create common MultiMessageLintCode class
class _LintCode extends LintCode {
  static final registry = <String, LintCode>{};

  factory _LintCode(String name, String message) => registry.putIfAbsent(
      name + message, () => new _LintCode._(name, message));

  _LintCode._(String name, String message) : super(name, message);
}

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

  final LinterContext context;

  _Visitor(this.rule, this.context);

  @override
  void visitSimpleIdentifier(SimpleIdentifier node) {
    // Should be "indexOf".
    final Element propertyElement = node.staticElement;
    if (propertyElement?.name != 'indexOf') {
      return;
    }

    AstNode indexOfAccess;
    InterfaceType type;

    final AstNode parent = node.parent;
    if (parent is MethodInvocation && node == parent.methodName) {
      indexOfAccess = parent;
      if (parent.target?.staticType is! InterfaceType) {
        return;
      }
      type = parent.target?.staticType;
    } else {
      return;
    }

    if (!DartTypeUtilities.implementsAnyInterface(
        type, <InterfaceTypeDefinition>[
      new InterfaceTypeDefinition('Iterable', 'dart.core'),
      new InterfaceTypeDefinition('String', 'dart.core'),
    ])) {
      return;
    }

    // Going up in AST structure to find binary comparison operator for this
    // `indexOf` access. Most of the time it will be a parent, but sometimes
    // it can be wrapped in parentheses or `as` operator.
    AstNode search = indexOfAccess;
    while (
        search != null && search is Expression && search is! BinaryExpression) {
      search = search.parent;
    }

    if (search is! BinaryExpression) {
      return;
    }

    final BinaryExpression binaryExpression = search;
    final Token operator = binaryExpression.operator;

    final TypeProvider typeProvider = context.typeProvider;
    final TypeSystem typeSystem = context.typeSystem;

    final DeclaredVariables declaredVariables = context.declaredVariables;

    // Comparing constants with result of indexOf.

    final ConstantVisitor visitor = new ConstantVisitor(
        new ConstantEvaluationEngine(typeProvider, declaredVariables,
            typeSystem: typeSystem),
        new ErrorReporter(
            AnalysisErrorListener.NULL_LISTENER, rule.reporter.source));

    final DartObjectImpl rightValue =
        binaryExpression.rightOperand.accept(visitor);
    if (rightValue?.type?.name == 'int') {
      // Constant is on right side of comparison operator
      _checkConstant(binaryExpression, rightValue.toIntValue(), operator.type);
      return;
    }

    final DartObjectImpl leftValue =
        binaryExpression.leftOperand.accept(visitor);
    if (leftValue?.type?.name == 'int') {
      // Constants is on left side of comparison operator
      _checkConstant(binaryExpression, leftValue.toIntValue(),
          _invertedTokenType(operator.type));
    }
  }

  void _checkConstant(Expression expression, int value, TokenType type) {
    if (value == -1) {
      if (type == TokenType.EQ_EQ ||
          type == TokenType.BANG_EQ ||
          type == TokenType.LT_EQ ||
          type == TokenType.GT) {
        rule.reportLintWithDescription(expression, useContains);
      } else if (type == TokenType.LT) {
        // indexOf < -1 is always false
        rule.reportLintWithDescription(expression, alwaysFalse);
      } else if (type == TokenType.GT_EQ) {
        // indexOf >= -1 is always true
        rule.reportLintWithDescription(expression, alwaysTrue);
      }
    } else if (value == 0) {
      // 'indexOf >= 0' is same as 'contains',
      // and 'indexOf < 0' is same as '!contains'
      if (type == TokenType.GT_EQ || type == TokenType.LT) {
        rule.reportLintWithDescription(expression, useContains);
      }
    } else if (value < -1) {
      // 'indexOf' is always >= -1, so comparing with lesser values makes
      // no sense.
      if (type == TokenType.EQ_EQ ||
          type == TokenType.LT_EQ ||
          type == TokenType.LT) {
        rule.reportLintWithDescription(expression, alwaysFalse);
      } else if (type == TokenType.BANG_EQ ||
          type == TokenType.GT_EQ ||
          type == TokenType.GT) {
        rule.reportLintWithDescription(expression, alwaysTrue);
      }
    }
  }

  TokenType _invertedTokenType(TokenType type) {
    switch (type) {
      case TokenType.LT_EQ:
        return TokenType.GT_EQ;

      case TokenType.LT:
        return TokenType.GT;

      case TokenType.GT:
        return TokenType.LT;

      case TokenType.GT_EQ:
        return TokenType.LT_EQ;

      default:
        return type;
    }
  }
}
