// 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:analysis_server/src/protocol_server.dart' hide Element;
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analysis_server_plugin/edit/correction_utils.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:collection/collection.dart';

/// An enumeration of possible postfix completion kinds.
abstract final class DartPostfixCompletion {
  static const _noTemplate = PostfixCompletionKind(
    '',
    'no change',
    _false,
    _null,
  );

  static const List<PostfixCompletionKind> allTemplates = [
    PostfixCompletionKind(
      'assert',
      'expr.assert -> assert(expr);',
      isAssertContext,
      expandAssert,
    ),
    PostfixCompletionKind(
      'fori',
      'limit.fori -> for(var i = 0; i < limit; i++) {}',
      isIntContext,
      expandFori,
    ),
    PostfixCompletionKind(
      'for',
      'values.for -> for(var value in values) {}',
      isIterableContext,
      expandFor,
    ),
    PostfixCompletionKind(
      'iter',
      'values.iter -> for(var value in values) {}',
      isIterableContext,
      expandFor,
    ),
    PostfixCompletionKind(
      'not',
      'bool.not -> !bool',
      isBoolContext,
      expandNegate,
    ),
    PostfixCompletionKind('!', 'bool! -> !bool', isBoolContext, expandNegate),
    PostfixCompletionKind(
      'else',
      'bool.else -> if (!bool) {}',
      isBoolContext,
      expandElse,
    ),
    PostfixCompletionKind(
      'if',
      'bool.if -> if (bool) {}',
      isBoolContext,
      expandIf,
    ),
    PostfixCompletionKind(
      'nn',
      'expr.nn -> if (expr != null) {}',
      isObjectContext,
      expandNotNull,
    ),
    PostfixCompletionKind(
      'notnull',
      'expr.notnull -> if (expr != null) {}',
      isObjectContext,
      expandNotNull,
    ),
    PostfixCompletionKind(
      'null',
      'expr.null -> if (expr == null) {}',
      isObjectContext,
      expandNull,
    ),
    PostfixCompletionKind(
      'par',
      'expr.par -> (expr)',
      isObjectContext,
      expandParen,
    ),
    PostfixCompletionKind(
      'return',
      'expr.return -> return expr',
      isObjectContext,
      expandReturn,
    ),
    PostfixCompletionKind(
      'switch',
      'expr.switch -> switch (expr) {}',
      isSwitchContext,
      expandSwitch,
    ),
    PostfixCompletionKind(
      'try',
      'stmt.try -> try {stmt} catch (e,s) {}',
      isStatementContext,
      expandTry,
    ),
    PostfixCompletionKind(
      'tryon',
      'stmt.try -> try {stmt} on Exception catch (e,s) {}',
      isStatementContext,
      expandTryon,
    ),
    PostfixCompletionKind(
      'while',
      'expr.while -> while (expr) {}',
      isBoolContext,
      expandWhile,
    ),
  ];

  static Future<PostfixCompletion?> expandAssert(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(kind, processor.findAssertExpression, (expr) {
      return 'assert(${processor.utils.getNodeText(expr)});';
    }, withBraces: false);
  }

  static Future<PostfixCompletion?> expandElse(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findBoolExpression,
      (expr) => 'if (${processor.makeNegatedBoolExpr(expr)})',
    );
  }

  static Future<PostfixCompletion?> expandFor(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(kind, processor.findIterableExpression, (expr) {
      var value = processor.newVariable('value');
      return 'for (var $value in ${processor.utils.getNodeText(expr)})';
    });
  }

  static Future<PostfixCompletion?> expandFori(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(kind, processor.findIntExpression, (expr) {
      var index = processor.newVariable('i');
      return 'for (int $index = 0; $index < ${processor.utils.getNodeText(expr)}; $index++)';
    });
  }

  static Future<PostfixCompletion?> expandIf(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findBoolExpression,
      (expr) => 'if (${processor.utils.getNodeText(expr)})',
    );
  }

  static Future<PostfixCompletion?> expandNegate(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findBoolExpression,
      (expr) => processor.makeNegatedBoolExpr(expr),
      withBraces: false,
    );
  }

  static Future<PostfixCompletion?> expandNotNull(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(kind, processor.findObjectExpression, (expr) {
      return expr is NullLiteral
          ? 'if (false)'
          : 'if (${processor.utils.getNodeText(expr)} != null)';
    });
  }

  static Future<PostfixCompletion?> expandNull(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(kind, processor.findObjectExpression, (expr) {
      return expr is NullLiteral
          ? 'if (true)'
          : 'if (${processor.utils.getNodeText(expr)} == null)';
    });
  }

  static Future<PostfixCompletion?> expandParen(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findObjectExpression,
      (expr) => '(${processor.utils.getNodeText(expr)})',
      withBraces: false,
    );
  }

  static Future<PostfixCompletion?> expandReturn(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findObjectExpression,
      (expr) => 'return ${processor.utils.getNodeText(expr)};',
      withBraces: false,
    );
  }

  static Future<PostfixCompletion?> expandSwitch(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findObjectExpression,
      (expr) => 'switch (${processor.utils.getNodeText(expr)})',
    );
  }

  static Future<PostfixCompletion?> expandTry(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expandTry(kind, processor.findStatement);
  }

  static Future<PostfixCompletion?> expandTryon(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expandTry(kind, processor.findStatement, withOn: true);
  }

  static Future<PostfixCompletion?> expandWhile(
    PostfixCompletionProcessor processor,
    PostfixCompletionKind kind,
  ) {
    return processor.expand(
      kind,
      processor.findBoolExpression,
      (expr) => 'while (${processor.utils.getNodeText(expr)})',
    );
  }

  static PostfixCompletionKind? forKey(String key) =>
      allTemplates.firstWhereOrNull((kind) => kind.key == key);

  static bool isAssertContext(PostfixCompletionProcessor processor) {
    return processor.findAssertExpression() != null;
  }

  static bool isBoolContext(PostfixCompletionProcessor processor) {
    return processor.findBoolExpression() != null;
  }

  static bool isIntContext(PostfixCompletionProcessor processor) {
    return processor.findIntExpression() != null;
  }

  static bool isIterableContext(PostfixCompletionProcessor processor) {
    return processor.findIterableExpression() != null;
  }

  static bool isObjectContext(PostfixCompletionProcessor processor) {
    return processor.findObjectExpression() != null;
  }

  static bool isStatementContext(PostfixCompletionProcessor processor) {
    return processor.findStatement() != null;
  }

  static bool isSwitchContext(PostfixCompletionProcessor processor) {
    return processor.findObjectExpression() != null;
  }

  static bool _false(PostfixCompletionProcessor _) => false;

  static Future<PostfixCompletion?> _null(
    PostfixCompletionProcessor _,
    PostfixCompletionKind _,
  ) async => null;
}

/// A description of a postfix completion.
///
/// Clients may not extend, implement or mix-in this class.
class PostfixCompletion {
  /// A description of the assist being proposed.
  final PostfixCompletionKind kind;

  /// The change to be made in order to apply the assist.
  final SourceChange change;

  /// Initialize a newly created completion to have the given [kind] and
  /// [change].
  new(this.kind, this.change);
}

/// The context for computing a postfix completion.
class PostfixCompletionContext {
  final ResolvedUnitResult resolveResult;
  final int selectionOffset;
  final String key;

  new(this.resolveResult, this.selectionOffset, this.key);
}

/// A description of a template for postfix completion. Instances are intended
/// to hold the functions required to determine applicability and expand the
/// template, in addition to its name and simple example. The example is shown
/// (in IntelliJ) in a code-completion menu, so must be quite short.
///
/// Clients may not extend, implement or mix-in this class.
class PostfixCompletionKind {
  final String name, example;
  final bool Function(PostfixCompletionProcessor) selector;
  final Future<PostfixCompletion?> Function(
    PostfixCompletionProcessor,
    PostfixCompletionKind,
  )
  computer;

  const new(this.name, this.example, this.selector, this.computer);

  String get key => name == '!' ? name : '.$name';

  String get message => 'Expand $key';

  @override
  String toString() => name;
}

/// The computer for Dart postfix completions.
final class PostfixCompletionProcessor {
  static final _noCompletion = PostfixCompletion(
    DartPostfixCompletion._noTemplate,
    SourceChange('', edits: []),
  );

  final PostfixCompletionContext _completionContext;
  final CorrectionUtils utils;
  AstNode? _node;
  PostfixCompletion? _completion;

  new(this._completionContext)
    : utils = CorrectionUtils(_completionContext.resolveResult);

  String get _eol => utils.endOfLine;

  String get _file => _completionContext.resolveResult.path;

  String get _key => _completionContext.key;

  int get _selectionOffset => _completionContext.selectionOffset;

  AnalysisSession get _session => _completionContext.resolveResult.session;

  TypeProvider get _typeProvider =>
      _completionContext.resolveResult.typeProvider;

  TypeSystem get _typeSystem => _completionContext.resolveResult.typeSystem;

  CompilationUnit get _unit => _completionContext.resolveResult.unit;

  Future<PostfixCompletion> compute() async {
    _node = _unit.nodeCovering(offset: _selectionOffset);
    if (_node == null) {
      return _noCompletion;
    }
    var completer = DartPostfixCompletion.forKey(_key);
    if (completer == null) {
      return _noCompletion;
    }
    return await completer.computer(this, completer) ?? _noCompletion;
  }

  Future<PostfixCompletion?> expand(
    PostfixCompletionKind kind,
    Expression? Function() contexter,
    String Function(Expression) sourcer, {
    bool withBraces = true,
  }) async {
    var expr = contexter();
    if (expr == null) {
      return null;
    }

    var changeBuilder = ChangeBuilder(session: _session);
    await changeBuilder.addDartFileEdit(_file, (builder) {
      builder.addReplacement(range.node(expr), (builder) {
        var newSrc = sourcer(expr);
        builder.write(newSrc);
        if (withBraces) {
          builder.write(' {');
          builder.write(_eol);
          var indent = utils.getNodePrefix(expr);
          builder.write(indent);
          builder.write(utils.oneIndent);
          builder.selectHere();
          builder.write(_eol);
          builder.write(indent);
          builder.write('}');
        } else {
          builder.selectHere();
        }
      });
    });
    _setCompletionFromBuilder(changeBuilder, kind);
    return _completion;
  }

  Future<PostfixCompletion?> expandTry(
    PostfixCompletionKind kind,
    Statement? Function() contexter, {
    bool withOn = false,
  }) async {
    var stmt = contexter();
    if (stmt == null) {
      return null;
    }
    var changeBuilder = ChangeBuilder(session: _session);
    await changeBuilder.addDartFileEdit(_file, (builder) {
      var lineInfo = _completionContext.resolveResult.lineInfo;
      // Embed the full line(s) of the statement in the try block.
      var startLine = lineInfo.getLocation(stmt.offset).lineNumber - 1;
      var endLine = lineInfo.getLocation(stmt.end).lineNumber - 1;
      if (stmt is ExpressionStatement) {
        var semicolon = stmt.semicolon;
        if (semicolon != null && !semicolon.isSynthetic) {
          endLine += 1;
        }
      }
      var startOffset = lineInfo.getOffsetOfLine(startLine);
      var endOffset = lineInfo.getOffsetOfLine(endLine);
      var src = utils.getText(startOffset, endOffset - startOffset);
      var indent = utils.getLinePrefix(stmt.offset);
      builder.addReplacement(
        range.startOffsetEndOffset(startOffset, endOffset),
        (builder) {
          builder.write(indent);
          builder.write('try {');
          builder.write(_eol);
          builder.write(
            utils.replaceSourceIndent(
              src,
              indent,
              '$indent${utils.oneIndent}',
              includeLeading: true,
              ensureTrailingNewline: true,
            ),
          );
          builder.selectHere();
          builder.write(indent);
          builder.write('}');
          if (withOn) {
            builder.write(' on ');
            builder.addSimpleLinkedEdit('NAME', nameOfExceptionThrownBy(stmt));
          }
          builder.write(' catch (e, s) {');
          builder.write(_eol);
          builder.write(indent);
          builder.write(utils.oneIndent);
          builder.write('print(s);');
          builder.write(_eol);
          builder.write(indent);
          builder.write('}');
          builder.write(_eol);
        },
      );
    });
    _setCompletionFromBuilder(changeBuilder, kind);
    return _completion;
  }

  Expression? findAssertExpression() {
    if (_node is Expression) {
      var boolExpr = _findOuterExpression(_node, _typeProvider.boolType);
      if (boolExpr == null) {
        return null;
      }
      var parent = boolExpr.parent;
      var grandParent = parent?.parent;
      if (parent is ExpressionFunctionBody &&
          grandParent is FunctionExpression) {
        var type = grandParent.staticType;
        if (type is! FunctionType) {
          return boolExpr;
        }
        if (type.returnType == _typeProvider.boolType) {
          return grandParent;
        }
      }
      if (boolExpr.staticType == _typeProvider.boolType) {
        return boolExpr;
      }
    }
    return null;
  }

  Expression? findBoolExpression() =>
      _findOuterExpression(_node, _typeProvider.boolType);

  Expression? findIntExpression() =>
      _findOuterExpression(_node, _typeProvider.intType);

  Expression? findIterableExpression() =>
      _findOuterExpression(_node, _typeProvider.iterableDynamicType);

  Expression? findObjectExpression() =>
      _findOuterExpression(_node, _typeProvider.objectQuestionType);

  Statement? findStatement() {
    var astNode = _node;
    while (astNode != null) {
      if (astNode is Statement && astNode is! Block) {
        // Disallow control-flow statements.
        if (astNode is DoStatement ||
            astNode is IfStatement ||
            astNode is ForStatement ||
            astNode is SwitchStatement ||
            astNode is TryStatement ||
            astNode is WhileStatement) {
          return null;
        }
        return astNode;
      }
      astNode = astNode.parent;
    }
    return null;
  }

  Future<bool> isApplicable() async {
    _node = _unit.nodeCovering(offset: _selectionOffset);
    if (_node == null) {
      return false;
    }

    var offset = _completionContext.selectionOffset;
    if (_node?.commentTokenCovering(offset) != null) {
      return false;
    }

    var completer = DartPostfixCompletion.forKey(_key);
    if (completer == null) {
      return false;
    }
    return completer.selector(this);
  }

  String makeNegatedBoolExpr(Expression expr) {
    var originalSrc = utils.getNodeText(expr);
    var newSrc = utils.invertCondition(expr);
    if (newSrc != originalSrc) {
      return newSrc;
    } else {
      return '!${utils.getNodeText(expr)}';
    }
  }

  String nameOfExceptionThrownBy(AstNode astNode) {
    if (astNode is ExpressionStatement) {
      astNode = astNode.expression;
    }
    if (astNode is ThrowExpression) {
      var expr = astNode;
      var type = expr.expression.staticType;
      if (type is! TypeImpl) {
        return 'Exception';
      }

      // Can't catch nullable types, strip `?`s now that we've checked for `*`s.
      return type.withNullability(NullabilitySuffix.none).getDisplayString();
    }
    return 'Exception';
  }

  String newVariable(String base) {
    var name = base;
    var i = 1;
    var vars = _unit.findPossibleLocalVariableConflicts(_selectionOffset);
    while (vars.contains(name)) {
      name = '$base${i++}';
    }
    return name;
  }

  Expression? _findOuterExpression(AstNode? start, InterfaceType builtInType) {
    if (start is SimpleIdentifier && start.element is PrefixElement) {
      return null;
    }

    AstNode? parent;
    if (start is Expression) {
      parent = start;
    } else if (start is ArgumentList) {
      parent = start.parent;
    }
    if (parent == null) {
      return null;
    }

    var list = <Expression>[];
    while (parent is Expression) {
      list.add(parent);
      parent = parent.parent;
    }

    var expr = list.firstWhereOrNull((expr) {
      var type = expr.staticType;
      if (type == null) return false;
      return _typeSystem.isSubtypeOf(type, builtInType);
    });
    var exprParent = expr?.parent;
    if (expr is SimpleIdentifier && exprParent is PropertyAccess) {
      expr = exprParent;
    }
    if (exprParent is CascadeExpression) {
      expr = exprParent;
    }
    return expr;
  }

  void _setCompletionFromBuilder(
    ChangeBuilder builder,
    PostfixCompletionKind kind,
  ) {
    var change = builder.sourceChange;
    if (change.edits.isEmpty) {
      _completion = null;
      return;
    }
    change.message = formatList(kind.message, null);
    _completion = PostfixCompletion(kind, change);
  }
}
