// Copyright (c) 2014, 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 'dart:async';

import 'package:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/services/correction/status.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/refactoring_internal.dart';
import 'package:analysis_server/src/services/search/search_engine.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/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/ast_provider.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';

/**
 * [InlineLocalRefactoring] implementation.
 */
class InlineLocalRefactoringImpl extends RefactoringImpl
    implements InlineLocalRefactoring {
  final SearchEngine searchEngine;
  final AstProvider astProvider;
  final CompilationUnit unit;
  final int offset;
  CompilationUnitElement unitElement;
  CorrectionUtils utils;

  Element _variableElement;
  VariableDeclaration _variableNode;
  List<SearchMatch> _references;

  InlineLocalRefactoringImpl(
      this.searchEngine, this.astProvider, this.unit, this.offset) {
    unitElement = unit.element;
    utils = new CorrectionUtils(unit);
  }

  @override
  String get refactoringName => 'Inline Local Variable';

  @override
  int get referenceCount {
    if (_references == null) {
      return 0;
    }
    return _references.length;
  }

  @override
  String get variableName {
    if (_variableElement == null) {
      return null;
    }
    return _variableElement.name;
  }

  @override
  Future<RefactoringStatus> checkFinalConditions() {
    RefactoringStatus result = new RefactoringStatus();
    return new Future.value(result);
  }

  @override
  Future<RefactoringStatus> checkInitialConditions() async {
    RefactoringStatus result = new RefactoringStatus();
    // prepare variable
    {
      AstNode offsetNode = new NodeLocator(offset).searchWithin(unit);
      if (offsetNode is SimpleIdentifier) {
        Element element = offsetNode.staticElement;
        if (element is LocalVariableElement) {
          _variableElement = element;
          AstNode name = await astProvider.getResolvedNameForElement(element);
          _variableNode = name.parent as VariableDeclaration;
        }
      }
    }
    // validate node declaration
    if (!_isVariableDeclaredInStatement()) {
      result = new RefactoringStatus.fatal(
          'Local variable declaration or reference must be selected '
          'to activate this refactoring.');
      return new Future<RefactoringStatus>.value(result);
    }
    // should have initializer at declaration
    if (_variableNode.initializer == null) {
      String message = format(
          "Local variable '{0}' is not initialized at declaration.",
          _variableElement.displayName);
      result = new RefactoringStatus.fatal(
          message, newLocation_fromNode(_variableNode));
      return new Future<RefactoringStatus>.value(result);
    }
    // prepare references
    _references = await searchEngine.searchReferences(_variableElement);
    // should not have assignments
    for (SearchMatch reference in _references) {
      if (reference.kind != MatchKind.READ) {
        String message = format(
            "Local variable '{0}' is assigned more than once.",
            [_variableElement.displayName]);
        return new RefactoringStatus.fatal(
            message, newLocation_fromMatch(reference));
      }
    }
    // done
    return result;
  }

  @override
  Future<SourceChange> createChange() {
    SourceChange change = new SourceChange(refactoringName);
    // remove declaration
    {
      Statement declarationStatement = _variableNode
          .getAncestor((node) => node is VariableDeclarationStatement);
      SourceRange range = utils.getLinesRangeStatements([declarationStatement]);
      doSourceChange_addElementEdit(
          change, unitElement, newSourceEdit_range(range, ''));
    }
    // prepare initializer
    Expression initializer = _variableNode.initializer;
    String initializerCode = utils.getNodeText(initializer);
    // replace references
    for (SearchMatch reference in _references) {
      SourceRange editRange = reference.sourceRange;
      // prepare context
      int offset = editRange.offset;
      AstNode node = utils.findNode(offset);
      AstNode parent = node.parent;
      // prepare code
      String codeForReference;
      if (parent is InterpolationExpression) {
        StringInterpolation target = parent.parent;
        if (initializer is SingleStringLiteral &&
            !initializer.isRaw &&
            initializer.isSingleQuoted == target.isSingleQuoted &&
            (!initializer.isMultiline || target.isMultiline)) {
          editRange = range.node(parent);
          // unwrap the literal being inlined
          int initOffset = initializer.contentsOffset;
          int initLength = initializer.contentsEnd - initOffset;
          codeForReference = utils.getText(initOffset, initLength);
        } else if (_shouldBeExpressionInterpolation(parent, initializer)) {
          codeForReference = '{$initializerCode}';
        } else {
          codeForReference = initializerCode;
        }
      } else if (_shouldUseParenthesis(initializer, node)) {
        codeForReference = '($initializerCode)';
      } else {
        codeForReference = initializerCode;
      }
      // do replace
      doSourceChange_addElementEdit(change, unitElement,
          newSourceEdit_range(editRange, codeForReference));
    }
    // done
    return new Future.value(change);
  }

  @override
  bool requiresPreview() => false;

  bool _isVariableDeclaredInStatement() {
    if (_variableNode == null) {
      return false;
    }
    AstNode parent = _variableNode.parent;
    if (parent is VariableDeclarationList) {
      parent = parent.parent;
      if (parent is VariableDeclarationStatement) {
        parent = parent.parent;
        return parent is Block || parent is SwitchCase;
      }
    }
    return false;
  }

  static bool _shouldBeExpressionInterpolation(
      InterpolationExpression target, Expression expression) {
    TokenType targetType = target.beginToken.type;
    return targetType == TokenType.STRING_INTERPOLATION_IDENTIFIER &&
        expression is! SimpleIdentifier;
  }

  static bool _shouldUseParenthesis(Expression init, AstNode node) {
    // check precedence
    int initPrecedence = getExpressionPrecedence(init);
    if (initPrecedence < getExpressionParentPrecedence(node)) {
      return true;
    }
    // special case for '-'
    AstNode parent = node.parent;
    if (init is PrefixExpression && parent is PrefixExpression) {
      if (parent.operator.type == TokenType.MINUS) {
        TokenType initializerOperator = init.operator.type;
        if (initializerOperator == TokenType.MINUS ||
            initializerOperator == TokenType.MINUS_MINUS) {
          return true;
        }
      }
    }
    // no () is needed
    return false;
  }
}
