// Copyright (c) 2015, 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/src/util/glob.dart';
import 'package:path/path.dart' as path;

/// Filter paths against a set of [_ignorePatterns] relative to a
/// [ignorePatternsRoot] directory. Paths outside of [includedRoot] are also
/// ignored.
class PathFilter {
  /// The path context to use when manipulating paths.
  final path.Context pathContext;

  /// The path in which files are considered to be included.
  final String includedRoot;

  /// Path that all ignore patterns are relative to.
  final String ignorePatternsRoot;

  /// List of ignore patterns that paths are tested against.
  final List<Glob> _ignorePatterns = <Glob>[];

  /// Construct a new path filter rooted at [includedRoot],
  /// with [ignorePatterns] that are relative to [ignorePatternsRoot].
  /// If [pathContext] is not specified, then the system path context is used.
  PathFilter(
    this.includedRoot,
    this.ignorePatternsRoot,
    List<String> ignorePatterns, [
    path.Context? pathContext,
  ]) : pathContext = pathContext ?? path.context {
    setIgnorePatterns(ignorePatterns);
  }

  /// Returns true if [path] should be ignored. A path is ignored if it is not
  /// contained in [includedRoot] or matches one of the ignore patterns.
  /// [path] is absolute or relative to [includedRoot].
  bool ignored(String path) {
    path = _canonicalize(path);
    return !_contained(path) || _match(path);
  }

  /// Set the ignore patterns.
  void setIgnorePatterns(List<String>? ignorePatterns) {
    _ignorePatterns.clear();
    if (ignorePatterns != null) {
      for (var ignorePattern in ignorePatterns) {
        _ignorePatterns.add(Glob(pathContext.separator, ignorePattern));
      }
    }
  }

  @override
  String toString() {
    StringBuffer sb = StringBuffer();
    for (Glob pattern in _ignorePatterns) {
      sb.write('$pattern ');
    }
    sb.writeln();
    return sb.toString();
  }

  /// Returns the absolute path of [path], relative to [includedRoot].
  String _canonicalize(String path) =>
      pathContext.normalize(pathContext.join(includedRoot, path));

  /// Returns true when [path] is contained inside [includedRoot].
  bool _contained(String path) => path.startsWith(includedRoot);

  /// Returns true if [path] matches any ignore patterns.
  bool _match(String path) {
    var relative = pathContext.relative(path, from: ignorePatternsRoot);
    for (Glob glob in _ignorePatterns) {
      if (glob.matches(relative)) {
        return true;
      }
    }
    return false;
  }
}
