// Copyright (c) 2024, 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:analysis_server/src/services/completion/dart/candidate_suggestion.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/completion_state.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_collector.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:path/path.dart' show posix;

/// The helper class that produces candidate suggestions based on the content
/// of the file system to complete within URIs in import, export and part
/// directives.
class UriHelper {
  /// The completion request being processed.
  final DartCompletionRequest request;

  /// The suggestion collector to which suggestions will be added.
  final SuggestionCollector collector;

  /// The state used to compute the candidate suggestions.
  final CompletionState state;

  new({required this.request, required this.collector, required this.state});

  void addSuggestions(StringLiteral uri) {
    if (uri is! SimpleStringLiteral) {
      return;
    }

    var offset = request.offset;
    var start = uri.offset;
    var end = uri.end;
    if (offset > start) {
      if (offset < end) {
        // Quoted non-empty string
        _simpleStringLiteral(uri);
      } else if (offset == end) {
        if (end == start + 1) {
          // Quoted empty string
          _simpleStringLiteral(uri);
        } else {
          var data = request.content;
          if (end == data.length) {
            var ch = data[end - 1];
            if (ch != '"' && ch != "'") {
              // Insertion point at end of file
              // and missing closing quote on non-empty string
              _simpleStringLiteral(uri);
            }
          }
        }
      }
    } else if (offset == start && offset == end) {
      var data = request.content;
      if (end == data.length) {
        var ch = data[end - 1];
        if (ch == '"' || ch == "'") {
          // Insertion point at end of file
          // and missing closing quote on empty string
          _simpleStringLiteral(uri);
        }
      }
    }
  }

  void _addDartSuggestions() {
    _suggestUri('dart:');
    var factory = request.sourceFactory;
    for (var lib in factory.dartSdk!.sdkLibraries) {
      if (!lib.isInternal && !lib.isImplementation) {
        if (!lib.shortName.startsWith('dart:_')) {
          _suggestUri(lib.shortName);
        }
      }
    }
  }

  void _addFileSuggestions(String partialUri) {
    var resProvider = request.resourceProvider;
    var resContext = resProvider.pathContext;
    var source = request.source;

    String parentUri;
    if (partialUri.endsWith('/')) {
      parentUri = partialUri;
    } else {
      parentUri = posix.dirname(partialUri);
      if (parentUri != '.' && !parentUri.endsWith('/')) {
        parentUri = '$parentUri/';
      }
    }
    var uriPrefix = parentUri == '.' ? '' : parentUri;

    // Only handle file uris in the format file:///xxx or /xxx
    var parentUriScheme = Uri.parse(parentUri).scheme;
    if (!parentUri.startsWith('file://') && parentUriScheme != '') {
      return;
    }

    var dirPath = resProvider.pathContext.fromUri(parentUri);
    dirPath = resContext.normalize(dirPath);

    if (resContext.isRelative(dirPath)) {
      var sourceDirPath = resContext.dirname(source.fullName);
      if (resContext.isAbsolute(sourceDirPath)) {
        dirPath = resContext.normalize(resContext.join(sourceDirPath, dirPath));
      } else {
        return;
      }
      // Do not suggest relative paths reaching outside the 'lib' directory.
      var srcInLib = resContext.split(sourceDirPath).contains('lib');
      var dstInLib = resContext.split(dirPath).contains('lib');
      if (srcInLib && !dstInLib) {
        return;
      }
    }
    if (dirPath.endsWith('\\.')) {
      dirPath = dirPath.substring(0, dirPath.length - 1);
    }

    var pathContext = request.resourceProvider.pathContext;
    var dir = resProvider.getResource(dirPath);
    if (dir is Folder) {
      try {
        for (var child in dir.getChildren()) {
          String? completion;
          if (child is Folder) {
            if (!child.shortName.startsWith('.')) {
              completion = '$uriPrefix${child.shortName}/';
            }
          } else if (child is File) {
            if (file_paths.isDart(pathContext, child.shortName)) {
              completion = '$uriPrefix${child.shortName}';
            }
          }
          if (completion != null && completion != source.shortName) {
            _suggestUri(completion);
          }
        }
      } on FileSystemException {
        // Guard against I/O exceptions.
      }
    }
  }

  void _addPackageFolderSuggestions(
    String partial,
    String prefix,
    Folder folder,
  ) {
    var pathContext = request.resourceProvider.pathContext;
    try {
      for (var child in folder.getChildren()) {
        if (child is Folder) {
          var childPrefix = '$prefix${child.shortName}/';
          _suggestUri(childPrefix);
          if (partial.startsWith(childPrefix)) {
            _addPackageFolderSuggestions(partial, childPrefix, child);
          }
        } else if (child is File) {
          if (file_paths.isDart(pathContext, child.path)) {
            _suggestUri('$prefix${child.shortName}');
          }
        }
      }
    } on FileSystemException {
      // Guard against I/O exceptions.
      return;
    }
  }

  void _addPackageSuggestions(String partial) {
    var factory = request.sourceFactory;
    var packageMap = factory.packageMap;
    if (packageMap != null) {
      _suggestUri('package:');
      packageMap.forEach((pkgName, folders) {
        var prefix = 'package:$pkgName/';
        _suggestUri(prefix);
        for (var folder in folders) {
          if (folder.exists) {
            _addPackageFolderSuggestions(partial, prefix, folder);
          }
        }
      });
    }
  }

  String? _extractPartialUri(SimpleStringLiteral node) {
    if (request.offset < node.contentsOffset) {
      return null;
    }
    return node.literal.lexeme.substring(
      node.contentsOffset - node.offset,
      request.offset - node.offset,
    );
  }

  void _simpleStringLiteral(SimpleStringLiteral node) {
    switch (node.parent) {
      case Configuration():
      case NamespaceDirective():
        var partialUri = _extractPartialUri(node);
        if (partialUri != null) {
          _addDartSuggestions();
          _addPackageSuggestions(partialUri);
          _addFileSuggestions(partialUri);
        }
      case PartDirective():
      case PartOfDirective():
        var partialUri = _extractPartialUri(node);
        if (partialUri != null) {
          _addFileSuggestions(partialUri);
        }
    }
  }

  void _suggestUri(String uriStr) {
    var matcherScore = state.matcher.score(uriStr);
    if (matcherScore != -1) {
      collector.addSuggestion(
        UriSuggestion(uriStr: uriStr, matcherScore: matcherScore),
      );
    }
  }
}
