// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io' as io
    show
        Directory,
        File,
        FileStat,
        FileSystemEntity,
        FileSystemEntityType,
        Link;

import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; // flutter_ignore: package_path_import

/// A [FileSystem] that wraps the [delegate] file system to create an overlay of
/// files from multiple [roots].
///
/// Regular paths or `file:` URIs are resolved directly in the underlying file
/// system, but URIs that use a special [scheme] are resolved by searching
/// under a set of given roots in order.
///
/// For example, consider the following inputs:
///
///   - scheme is `multi-root`
///   - the set of roots are `/a` and `/b`
///   - the underlying file system contains files:
///         /root_a/dir/only_a.dart
///         /root_a/dir/both.dart
///         /root_b/dir/only_b.dart
///         /root_b/dir/both.dart
///         /other/other.dart
///
/// Then:
///
///   - file:///other/other.dart is resolved as /other/other.dart
///   - multi-root:///dir/only_a.dart is resolved as /root_a/dir/only_a.dart
///   - multi-root:///dir/only_b.dart is resolved as /root_b/dir/only_b.dart
///   - multi-root:///dir/both.dart is resolved as /root_a/dir/only_a.dart
class MultiRootFileSystem extends ForwardingFileSystem {
  MultiRootFileSystem({
    required FileSystem delegate,
    required String scheme,
    required List<String> roots,
  })   : assert(roots.isNotEmpty),
        _scheme = scheme,
        _roots = roots.map((String root) => delegate.path.normalize(root)).toList(),
        super(delegate);

  @visibleForTesting
  FileSystem get fileSystem => delegate;

  final String _scheme;
  final List<String> _roots;

  @override
  File file(dynamic path) => MultiRootFile(
    fileSystem: this,
    delegate: delegate.file(_resolve(path)),
  );

  @override
  Directory directory(dynamic path) => MultiRootDirectory(
    fileSystem: this,
    delegate: delegate.directory(_resolve(path)),
  );

  @override
  Link link(dynamic path) => MultiRootLink(
    fileSystem: this,
    delegate: delegate.link(_resolve(path)),
  );

  @override
  Future<io.FileStat> stat(String path) =>
    delegate.stat(_resolve(path).toString());

  @override
  io.FileStat statSync(String path) =>
    delegate.statSync(_resolve(path).toString());

  @override
  Future<bool> identical(String path1, String path2) =>
    delegate.identical(_resolve(path1).toString(), _resolve(path2).toString());

  @override
  bool identicalSync(String path1, String path2) =>
    delegate.identicalSync(_resolve(path1).toString(), _resolve(path2).toString());

  @override
  Future<io.FileSystemEntityType> type(String path, {bool followLinks = true}) =>
    delegate.type(_resolve(path).toString(), followLinks: followLinks);

  @override
  io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) =>
    delegate.typeSync(_resolve(path).toString(), followLinks: followLinks);

  // Caching the path context here and clearing when the currentDirectory setter
  // is updated works since the flutter tool restricts usage of dart:io directly
  // via the forbidden import tests. Otherwise, the path context's current
  // working directory might get out of sync, leading to unexpected results from
  // methods like `path.relative`.
  @override
  p.Context get path => _cachedPath ??= delegate.path;
  p.Context? _cachedPath;

  @override
  set currentDirectory(dynamic path) {
    _cachedPath = null;
    delegate.currentDirectory = path;
  }

  /// If the path is a multiroot uri, resolve to the actual path of the
  /// underlying file system. Otherwise, return as is.
  dynamic _resolve(dynamic path) {
    Uri uri;
    if (path == null) {
      return null;
    } else if (path is String) {
      uri = Uri.parse(path);
    } else if (path is Uri) {
      uri = path;
    } else if (path is FileSystemEntity) {
      uri = path.uri;
    } else {
      throw ArgumentError('Invalid type for "path": ${(path as Object?)?.runtimeType}');
    }

    if (!uri.hasScheme || uri.scheme != _scheme) {
      return path;
    }

    String? firstRootPath;
    final String relativePath = delegate.path.joinAll(uri.pathSegments);
    for (final String root in _roots) {
      final String pathWithRoot = delegate.path.join(root, relativePath);
      if (delegate.typeSync(pathWithRoot, followLinks: false) !=
          FileSystemEntityType.notFound) {
        return pathWithRoot;
      }
      firstRootPath ??= pathWithRoot;
    }

    // If not found, construct the path with the first root.
    return firstRootPath!;
  }

  Uri _toMultiRootUri(Uri uri) {
    if (uri.scheme != 'file') {
      return uri;
    }

    final p.Context pathContext = delegate.path;
    final bool isWindows = pathContext.style == p.Style.windows;
    final String path = uri.toFilePath(windows: isWindows);
    for (final String root in _roots) {
      if (path.startsWith('$root${pathContext.separator}')) {
        String pathWithoutRoot = path.substring(root.length + 1);
        if (isWindows) {
          // Convert the path from Windows style
          pathWithoutRoot = p.url.joinAll(pathContext.split(pathWithoutRoot));
        }
        return Uri.parse('$_scheme:///$pathWithoutRoot');
      }
    }
    return uri;
  }

  @override
  String toString() =>
    'MultiRootFileSystem(scheme = $_scheme, roots = $_roots, delegate = $delegate)';
}

abstract class MultiRootFileSystemEntity<T extends FileSystemEntity,
    D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> {
  MultiRootFileSystemEntity({
    required this.fileSystem,
    required this.delegate,
  });

  @override
  final D delegate;

  @override
  final MultiRootFileSystem fileSystem;

  @override
  File wrapFile(io.File delegate) => MultiRootFile(
    fileSystem: fileSystem,
    delegate: delegate,
  );

  @override
  Directory wrapDirectory(io.Directory delegate) => MultiRootDirectory(
    fileSystem: fileSystem,
    delegate: delegate,
  );

  @override
  Link wrapLink(io.Link delegate) => MultiRootLink(
    fileSystem: fileSystem,
    delegate: delegate,
  );

  @override
  Uri get uri => fileSystem._toMultiRootUri(delegate.uri);
}

class MultiRootFile extends MultiRootFileSystemEntity<File, io.File>
    with ForwardingFile {
  MultiRootFile({
    required super.fileSystem,
    required super.delegate,
  });

  @override
  String toString() =>
    'MultiRootFile(fileSystem = $fileSystem, delegate = $delegate)';
}

class MultiRootDirectory
    extends MultiRootFileSystemEntity<Directory, io.Directory>
    with ForwardingDirectory<Directory> {
  MultiRootDirectory({
    required super.fileSystem,
    required super.delegate,
  });

  // For the childEntity methods, we first obtain an instance of the entity
  // from the underlying file system, then invoke childEntity() on it, then
  // wrap in the ErrorHandling version.
  @override
  Directory childDirectory(String basename) =>
    fileSystem.directory(fileSystem.path.join(delegate.path, basename));

  @override
  File childFile(String basename) =>
    fileSystem.file(fileSystem.path.join(delegate.path, basename));

  @override
  Link childLink(String basename) =>
    fileSystem.link(fileSystem.path.join(delegate.path, basename));

  @override
  String toString() =>
    'MultiRootDirectory(fileSystem = $fileSystem, delegate = $delegate)';
}

class MultiRootLink
    extends MultiRootFileSystemEntity<Link, io.Link>
    with ForwardingLink {
  MultiRootLink({
    required super.fileSystem,
    required super.delegate,
  });

  @override
  String toString() =>
    'MultiRootLink(fileSystem = $fileSystem, delegate = $delegate)';
}
