// Copyright (c) 2020, 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/protocol_server.dart';
import 'package:analysis_server/src/services/pub/pub_package_service.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:path/path.dart' as path;

/// An object that represents the location of a Boolean value.
class BooleanProducer extends Producer {
  /// Initialize a location whose valid values are Booleans.
  const new();

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    yield identifier('true');
    yield identifier('false');
  }
}

/// An object that represents the location of an arbitrary value. They serve as
/// placeholders when there are no reasonable suggestions for a given location.
class EmptyProducer extends Producer {
  /// Initialize a location whose valid values are arbitrary.
  const new();

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    // Returns nothing.
  }
}

/// An object that represents the location of a value from a finite set of
/// choices.
class EnumProducer extends Producer {
  /// The list of valid values at this location.
  final List<String> values;

  /// Initialize a location whose valid values are in the list of [values].
  const new(this.values);

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    for (var value in values) {
      yield identifier(value);
    }
  }
}

/// An object that represents the location of a possibly relative file path.
class FilePathProducer extends Producer {
  /// Initialize a producer whose valid values are file paths.
  const new();

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    //
    // This currently assumes that all of the paths in the assets section will
    // be posix paths.
    //
    var context = path.posix;
    var separator = context.separator;
    var precedingText = request.precedingText;

    String parentDirectory;
    if (precedingText.isEmpty || precedingText.endsWith(separator)) {
      parentDirectory = precedingText;
    } else {
      parentDirectory = context.dirname(precedingText);
    }
    if (parentDirectory == '.') {
      parentDirectory = '';
    } else if (parentDirectory.endsWith(separator)) {
      parentDirectory = parentDirectory.substring(
        0,
        parentDirectory.length - separator.length,
      );
    }
    //
    // Convert from posix to the platform context.
    //
    var provider = request.resourceProvider;
    context = provider.pathContext;
    parentDirectory = context.joinAll(path.posix.split(parentDirectory));
    //
    // Resolve the relative path and access the disk to see what child entities
    // exist within the [parentDirectory] that can be suggested.
    //
    if (context.isRelative(parentDirectory)) {
      parentDirectory = context.join(
        context.dirname(request.filePath),
        parentDirectory,
      );
    }
    parentDirectory = context.normalize(parentDirectory);
    var dir = provider.getResource(parentDirectory);
    if (dir is Folder) {
      try {
        for (var child in dir.getChildren()) {
          var name = child.shortName;
          var relevance = name.startsWith('.') ? 500 : 1000;
          yield identifier(name, relevance: relevance);
        }
      } on FileSystemException {
        // Guard against I/O exceptions.
      }
    }
  }
}

/// An object that represents the location of the keys/values in a map.
abstract class KeyValueProducer extends Producer {
  /// Initialize a producer representing a key/value pair in a map.
  const new();

  /// Returns a producer for values of the given [key], or `null` if there is
  /// no registered producer for the [key].
  Producer? producerForKey(String key);
}

/// An object that represents the location of an element in a list.
class ListProducer extends Producer {
  /// The producer used to produce suggestions for an element of the list.
  final Producer element;

  /// Initialize a location whose valid values are determined by the [element]
  /// producer.
  const new(this.element);

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    for (var suggestion in element.suggestions(request)) {
      // TODO(brianwilkerson): Consider prepending the suggestion with a hyphen
      //  when the current node isn't already preceded by a hyphen. The
      //  cleanest way to do this is probably to access the [element] producer
      //  in the place where we're choosing a producer in that situation.
      // suggestion.completion = '- ${suggestion.completion}';
      yield suggestion;
    }
  }
}

/// An object that represents the location of the keys in a map.
class MapProducer extends KeyValueProducer {
  /// A table from the value of a key to the producer used to make suggestions
  /// for the value following the key.
  final Map<String, Producer> _children;

  /// Initialize a location whose valid values are the keys of a map as encoded
  /// by the map of [_children].
  const new(this._children);

  @override
  Producer? producerForKey(String key) => _children[key];

  @override
  Iterable<CompletionSuggestion> suggestions(
    YamlCompletionRequest request,
  ) sync* {
    for (var entry in _children.entries) {
      if (entry.value is ListProducer) {
        yield identifier('${entry.key}:');
      } else {
        yield identifier('${entry.key}: ');
      }
    }
  }
}

/// An object that represents a specific location in the structure of the valid
/// YAML representation and can produce completion suggestions appropriate for
/// that location.
abstract class Producer {
  /// Initialize a newly created instance of this class.
  const new();

  /// A utility method used to create a suggestion for the [identifier].
  CompletionSuggestion identifier(
    String identifier, {
    int relevance = 1000,
    String? docComplete,
  }) => CompletionSuggestion(
    CompletionSuggestionKind.IDENTIFIER,
    relevance,
    identifier,
    identifier.length,
    0,
    false,
    false,
    docComplete: docComplete,
  );

  /// A utility method used to create a suggestion for the package [packageName].
  CompletionSuggestion packageName(
    String packageName, {
    int relevance = 1000,
  }) => CompletionSuggestion(
    CompletionSuggestionKind.PACKAGE_NAME,
    relevance,
    packageName,
    packageName.length,
    0,
    false,
    false,
  );

  /// Return the completion suggestions appropriate to this location.
  Iterable<CompletionSuggestion> suggestions(YamlCompletionRequest request);
}

/// The information provided to a [Producer] when requesting completions.
class YamlCompletionRequest {
  /// The resource provider used to access the file system.
  final ResourceProvider resourceProvider;

  /// The Pub package service used for looking up package names/versions.
  final PubPackageService? pubPackageService;

  /// The absolute path of the file in which completions are being requested.
  final String filePath;

  /// The text to the left of the cursor.
  final String precedingText;

  /// Initialize a newly created completion request.
  new({
    required this.filePath,
    required this.precedingText,
    required this.resourceProvider,
    required this.pubPackageService,
  });
}
