// 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 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';

/// A visitor for visiting [AstNode]s covered by a selection [SourceRange].
class SelectionAnalyzer extends GeneralizingAstVisitor<void> {
  final SourceRange selection;

  AstNode? _coveringNode;
  List<AstNode> _selectedNodes = [];

  new(this.selection);

  /// Return the [AstNode] with the shortest length which completely covers the
  /// specified selection.
  AstNode? get coveringNode => _coveringNode;

  /// Returns the first selected [AstNode], may be `null`.
  AstNode? get firstSelectedNode {
    if (_selectedNodes.isEmpty) {
      return null;
    }
    return _selectedNodes[0];
  }

  /// Returns `true` if there are [AstNode]s fully covered by the
  /// selection [SourceRange].
  bool get hasSelectedNodes => _selectedNodes.isNotEmpty;

  /// Returns `true` if there was no selected nodes yet.
  bool get isFirstNode => _selectedNodes.isEmpty;

  /// Returns the last selected [AstNode], may be `null`.
  AstNode? get lastSelectedNode {
    if (_selectedNodes.isEmpty) {
      return null;
    }
    return _selectedNodes[_selectedNodes.length - 1];
  }

  /// Return the [AstNode]s fully covered by the selection [SourceRange].
  List<AstNode> get selectedNodes {
    return _selectedNodes;
  }

  /// Adds first selected [AstNode].
  void handleFirstSelectedNode(AstNode node) {
    _selectedNodes = [];
    _selectedNodes.add(node);
  }

  /// Adds second or more selected [AstNode].
  void handleNextSelectedNode(AstNode node) {
    if (firstSelectedNode?.parent == node.parent) {
      _selectedNodes.add(node);
    }
  }

  /// Notifies that selection ends in given [AstNode].
  void handleSelectionEndsIn(AstNode node) {}

  /// Notifies that selection starts in given [AstNode].
  void handleSelectionStartsIn(AstNode node) {}

  /// Resets selected nodes.
  void reset() {
    _selectedNodes = [];
  }

  @override
  void visitNode(AstNode node) {
    var nodeRange = range.node(node);
    if (selection.covers(nodeRange)) {
      if (isFirstNode) {
        handleFirstSelectedNode(node);
      } else {
        handleNextSelectedNode(node);
      }
      return;
    } else if (selection.coveredBy(nodeRange)) {
      _coveringNode = node;
      node.visitChildren(this);
      return;
    } else if (selection.startsIn(nodeRange)) {
      handleSelectionStartsIn(node);
      node.visitChildren(this);
      return;
    } else if (selection.endsIn(nodeRange)) {
      handleSelectionEndsIn(node);
      node.visitChildren(this);
      return;
    }
    // no intersection
  }
}
