import 'package:meta/meta.dart';

import '../core/parser.dart';
import 'analyzer.dart';
import 'internal/linter_rules.dart';

/// The type of a linter issue.
enum LinterType { info, warning, error }

/// Encapsulates a single linter rule.
@immutable
abstract class LinterRule {
  /// Constructs a new linter rule.
  const LinterRule(this.type, this.title);

  /// Severity of issues detected by this rule.
  final LinterType type;

  /// Human readable title of this rule.
  final String title;

  /// Executes this rule using a provided [analyzer] on a [parser]. Expected
  /// to call [callback] zero or more times as issues are detected.
  void run(Analyzer analyzer, Parser parser, LinterCallback callback);

  @override
  String toString() => '$runtimeType(type: $type, title: $title)';
}

/// Encapsulates a single linter issue.
@immutable
class LinterIssue {
  /// Constructs a new linter rule.
  const LinterIssue(this.rule, this.parser, this.description);

  /// Rule that identified the issue.
  final LinterRule rule;

  /// Severity of the issue.
  LinterType get type => rule.type;

  /// Title of the issue.
  String get title => rule.title;

  /// Parser object with the issue.
  final Parser parser;

  /// Detailed explanation of the issue.
  final String description;

  @override
  String toString() =>
      '$runtimeType(type: $type, title: $title, '
      'parser: $parser, description: $description)';
}

/// Function signature of a linter callback that is called whenever a linter
/// rule identifies an issue.
typedef LinterCallback = void Function(LinterIssue issue);

/// All default linter rules to be run.
const allLinterRules = [
  CharacterRepeater(),
  LeftRecursion(),
  NestedChoice(),
  NullableRepeater(),
  OverlappingChoice(),
  RepeatedChoice(),
  UnnecessaryFlatten(),
  UnnecessaryResolvable(),
  UnoptimizedFlatten(),
  UnreachableChoice(),
  UnresolvedSettable(),
  UnusedResult(),
];

/// Returns a list of linter issues found when analyzing the parser graph
/// reachable from [parser].
///
/// The optional [callback] is triggered during the search for each issue
/// discovered.
///
/// A custom list of [rules] can be provided, otherwise [allLinterRules] are
/// used and filtered by the set of [excludedRules] and [excludedTypes] (rules
/// of `LinterType.info` are ignored by default).
List<LinterIssue> linter(
  Parser parser, {
  LinterCallback? callback,
  List<LinterRule>? rules,
  Set<String> excludedRules = const {},
  Set<LinterType> excludedTypes = const {LinterType.info},
}) {
  final issues = <LinterIssue>[];
  final analyzer = Analyzer(parser);
  final selectedRules =
      rules ??
      allLinterRules
          .where(
            (rule) =>
                !excludedRules.contains(rule.title) &&
                !excludedTypes.contains(rule.type),
          )
          .toList(growable: false);
  for (final parser in analyzer.parsers) {
    for (final rule in selectedRules) {
      rule.run(analyzer, parser, (issue) {
        if (callback != null) {
          callback(issue);
        }
        issues.add(issue);
      });
    }
  }
  return issues;
}
