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() => 'LinterRule(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() => 'LinterIssue(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;
}
