// Copyright (c) 2021, 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/analysis_rule/pubspec.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/workspace/pub.dart';

abstract class FileStateFilter {
  /// Return a filter of files that can be accessed by the [file].
  factory FileStateFilter(FileState file) {
    var workspacePackage = file.workspacePackage;
    if (workspacePackage is PubPackage) {
      return _PubFilter(workspacePackage, file.path);
    } else {
      return _AnyFilter();
    }
  }

  bool shouldInclude(FileState file);

  static bool shouldIncludeSdk(FileState file, FileUriProperties uri) {
    assert(identical(file.uriProperties, uri));
    assert(uri.isDart);

    // Exclude internal libraries.
    if (uri.isDartInternal) {
      return false;
    }

    // Exclude "soft deprecated" libraries.
    if (const {
      'dart:html',
      'dart:indexed_db',
      'dart:js',
      'dart:js_util',
      'dart:svg',
      'dart:web_audio',
      'dart:web_gl',
    }.contains(file.uriStr)) {
      return false;
    }

    return true;
  }
}

class _AnyFilter implements FileStateFilter {
  @override
  bool shouldInclude(FileState file) {
    var uri = file.uriProperties;
    if (uri.isDart) {
      return FileStateFilter.shouldIncludeSdk(file, uri);
    }
    return true;
  }
}

class _PubFilter implements FileStateFilter {
  final PubPackage targetPackage;
  final String? targetPackageName;

  /// "Friends of `package:analyzer` see analyzer implementation libraries in
  /// completions.
  final bool targetPackageIsFriendOfAnalyzer;
  final bool targetInLibOrEntryPoint;
  final Set<String> dependencies;

  factory _PubFilter(PubPackage package, String path) {
    var packageRootFolder = package.root;
    var inLibOrEntryPoint =
        packageRootFolder.getChildAssumingFolder('lib').contains(path) ||
        packageRootFolder.getChildAssumingFolder('bin').contains(path) ||
        packageRootFolder.getChildAssumingFolder('web').contains(path);

    var dependencies = <String>{};
    var pubspec = package.pubspec;
    if (pubspec != null) {
      dependencies.addAll(pubspec.dependencies.names);
      if (!inLibOrEntryPoint) {
        dependencies.addAll(pubspec.devDependencies.names);
      }
    }

    var packageName = pubspec?.name?.value.text;

    return _PubFilter._(
      targetPackage: package,
      targetPackageName: packageName,
      targetPackageIsFriendOfAnalyzer:
          packageName == 'analysis_server' || packageName == 'linter',
      targetInLibOrEntryPoint: inLibOrEntryPoint,
      dependencies: dependencies,
    );
  }

  _PubFilter._({
    required this.targetPackage,
    required this.targetPackageName,
    required this.targetPackageIsFriendOfAnalyzer,
    required this.targetInLibOrEntryPoint,
    required this.dependencies,
  });

  @override
  bool shouldInclude(FileState file) {
    var uri = file.uriProperties;
    if (uri.isDart) {
      return FileStateFilter.shouldIncludeSdk(file, uri);
    }

    // Normally only package URIs are available.
    // But outside of lib/ and entry points we allow any files of this package.
    var packageName = uri.packageName;
    if (packageName == null) {
      if (targetInLibOrEntryPoint) {
        return false;
      } else {
        var filePackage = file.workspacePackage;
        return filePackage is PubPackage &&
            filePackage.root == targetPackage.root;
      }
    }

    // Any `package:` library from the same package.
    if (packageName == targetPackageName) {
      return true;
    }

    // If not the same package, must be public.
    if (uri.isSrc) {
      // Special case access to `analyzer` to allow privileged access
      // from "friends" like `analysis_server` and `linter`.
      if (targetPackageIsFriendOfAnalyzer && packageName == 'analyzer') {
        return true;
      }
      return false;
    }

    return dependencies.contains(packageName);
  }
}

extension on PubspecDependencyList? {
  List<String> get names {
    var self = this;
    if (self == null) {
      return const [];
    } else {
      return self.map((dependency) => dependency.name?.text).nonNulls.toList();
    }
  }
}
