content: // Copyright 2017 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'dart:async';
  
  import 'package:cli_pkg/js.dart';
  import 'package:js/js.dart';
  import 'package:path/path.dart' as p;
  
  import '../../io.dart';
  import '../../js/function.dart';
  import '../../js/legacy/importer_result.dart';
  import '../../js/legacy/render_context.dart';
  import '../../js/utils.dart';
  import '../../util/nullable.dart';
  import '../utils.dart';
  
  /// An importer that encapsulates Node Sass's import logic.
  ///
  /// This isn't a normal [Importer] because Node Sass's import behavior isn't
  /// compatible with Dart Sass's. In particular:
  ///
  /// * Rather than doing URL resolution for relative imports, the importer is
  ///   passed the URL of the file that contains the import which it can then use
  ///   to do its own relative resolution. It's passed this even if that file was
  ///   imported by a different importer.
  ///
  /// * Importers can return file paths rather than the contents of the imported
  ///   file. These paths are made absolute before they're passed as the previous
  ///   "URL" to other importers.
  ///
  /// * The working directory is always implicitly an include path.
  ///
  /// * The order of import precedence is as follows:
  ///
  ///   1. Filesystem imports relative to the base file.
  ///   2. Custom importer imports.
  ///   3. Filesystem imports relative to the working directory.
  ///   4. Filesystem imports relative to an `includePaths` path.
  ///   5. Filesystem imports relative to a `SASS_PATH` path.
  final class NodeImporter {
    /// The options for the `this` context in which importer functions are
    /// invoked.
    ///
    /// This is typed as [Object] because the public interface of [NodeImporter]
    /// is shared with the VM, which can't handle JS interop types.
    final Object _options;
  
    /// The include paths passed in by the user.
    final List<String> _includePaths;
  
    /// The importer functions passed in by the user.
    final List<JSFunction> _importers;
  
    NodeImporter(
      this._options,
      Iterable<String> includePaths,
      Iterable<Object> importers,
    )   : _includePaths = List.unmodifiable(_addSassPath(includePaths)),
          _importers = List.unmodifiable(importers.cast());
  
    /// Returns [includePaths] followed by any paths loaded from the `SASS_PATH`
    /// environment variable.
    static Iterable<String> _addSassPath(Iterable<String> includePaths) sync* {
      yield* includePaths;
      var sassPath = getEnvironmentVariable("SASS_PATH");
      if (sassPath == null) return;
      yield* sassPath.split(isWindows ? ';' : ':');
    }
  
    /// Loads the stylesheet at [url] relative to [previous] if possible.
    ///
    /// This can also load [url] directly if it's an absolute `file:` URL, even if
    /// `previous` isn't defined or isn't a `file:` URL.
    ///
    /// Returns the stylesheet at that path and the URL used to load it, or `null`
    /// if loading failed.
    (String contents, String url)? loadRelative(
      String url,
      Uri? previous,
      bool forImport,
    ) {
      if (p.url.isAbsolute(url)) {
        if (!url.startsWith('/') && !url.startsWith('file:')) return null;
        return _tryPath(p.fromUri(url), forImport);
      }
  
      if (previous?.scheme != 'file') return null;
  
      // 1: Filesystem imports relative to the base file.
      return _tryPath(
        p.join(p.dirname(p.fromUri(previous)), p.fromUri(url)),
        forImport,
      );
    }
  
    /// Loads the stylesheet at [url] from an importer or load path.
    ///
    /// The [previous] URL is the URL of the stylesheet in which the import
    /// appeared. Returns the contents of the stylesheet and the URL to use as
    /// [previous] for imports within the loaded stylesheet.
    (String contents, String url)? load(
      String url,
      Uri? previous,
      bool forImport,
    ) {
      // The previous URL is always an absolute file path for filesystem imports.
      var previousString = _previousToString(previous);
      for (var importer in _importers) {
        if (wrapJSExceptions(
          () => call2(importer, _renderContext(forImport), url, previousString),
        )
            case var value?) {
          return _handleImportResult(url, previous, value, forImport);
        }
      }
  
      return _resolveLoadPathFromUrl(Uri.parse(url), forImport);
    }
  
    /// Asynchronously loads the stylesheet at [url] from an importer or load
    /// path.
    ///
    /// The [previous] URL is the URL of the stylesheet in which the import
    /// appeared. Returns the contents of the stylesheet and the URL to use as
    /// [previous] for imports within the loaded stylesheet.
    Future<(String contents, String url)?> loadAsync(
      String url,
      Uri? previous,
      bool forImport,
    ) async {
      // The previous URL is always an absolute file path for filesystem imports.
      var previousString = _previousToString(previous);
      for (var importer in _importers) {
        if (await _callImporterAsync(importer, url, previousString, forImport)
            case var value?) {
          return _handleImportResult(url, previous, value, forImport);
        }
      }
  
      return _resolveLoadPathFromUrl(Uri.parse(url), forImport);
    }
  
    /// Converts [previous] to a string to pass to the importer function.
    String _previousToString(Uri? previous) => switch (previous) {
          null => 'stdin',
          Uri(scheme: 'file') => p.fromUri(previous),
          _ => previous.toString(),
        };
  
    /// Tries to load a stylesheet at the given [url] from a load path (including
    /// the working directory), if that URL refers to the filesystem.
    ///
    /// Returns the stylesheet at that path and the URL used to load it, or `null`
    /// if loading failed.
    (String, String)? _resolveLoadPathFromUrl(Uri url, bool forImport) =>
        url.scheme == '' || url.scheme == 'file'
            ? _resolveLoadPath(p.fromUri(url), forImport)
            : null;
  
    /// Tries to load a stylesheet at the given [path] from a load path (including
    /// the working directory).
    ///
    /// Returns the stylesheet at that path and the URL used to load it, or `null`
    /// if loading failed.
    (String, String)? _resolveLoadPath(String path, bool forImport) {
      // 2: Filesystem imports relative to the working directory.
      if (_tryPath(p.absolute(path), forImport) case var result?) return result;
  
      // 3: Filesystem imports relative to [_includePaths].
      for (var includePath in _includePaths) {
        if (_tryPath(p.absolute(p.join(includePath, path)), forImport)
            case var result?) {
          return result;
        }
      }
  
      return null;
    }
  
    /// Tries to load a stylesheet at the given [path].
    ///
    /// Returns the stylesheet at that path and the URL used to load it, or `null`
    /// if loading failed.
    (String, String)? _tryPath(String path, bool forImport) => (forImport
                ? inImportRule(() => resolveImportPath(path))
                : resolveImportPath(path))
            .andThen(
          (resolved) => (readFile(resolved), p.toUri(resolved).toString()),
        );
  
    /// Converts an importer's return [value] to a (contents, url) pair that can
    /// be returned by [load].
    (String, String)? _handleImportResult(
      String url,
      Uri? previous,
      Object value,
      bool forImport,
    ) {
      if (isJSError(value)) throw value;
      if (value is! NodeImporterResult) return null;
  
      var file = value.file;
      var contents = value.contents;
      if (contents != null && !isJsString(contents)) {
        jsThrow(
          ArgumentError.value(
            contents,
            'contents',
            'must be a string but was: ${jsType(contents)}',
          ),
        );
      }
  
      if (file == null) {
        return (contents ?? '', url);
      } else if (contents != null) {
        return (contents, p.toUri(file).toString());
      } else {
        var resolved =
            loadRelative(p.toUri(file).toString(), previous, forImport) ??
                _resolveLoadPath(file, forImport);
        if (resolved != null) return resolved;
        throw "Can't find stylesheet to import.";
      }
    }
  
    /// Calls an importer that may or may not be asynchronous.
    Future<Object?> _callImporterAsync(
      JSFunction importer,
      String url,
      String previousString,
      bool forImport,
    ) async {
      var completer = Completer<Object>();
  
      var result = wrapJSExceptions(
        () => call3(
          importer,
          _renderContext(forImport),
          url,
          previousString,
          allowInterop(completer.complete),
        ),
      );
      if (isUndefined(result)) return await completer.future;
      return result;
    }
  
    /// Returns the [RenderContext] in which to invoke importers.
    RenderContext _renderContext(bool fromImport) {
      var context = RenderContext(
        options: _options as RenderContextOptions,
        fromImport: fromImport,
      );
      context.options.context = context;
      return context;
    }
  }
  